Merge pull request #945 from vector-im/feature/stabilization_2
Feature/stabilization 2
This commit is contained in:
commit
3189c114dc
1
.idea/codeStyles/Project.xml
generated
1
.idea/codeStyles/Project.xml
generated
@ -1,5 +1,6 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
|
<option name="RIGHT_MARGIN" value="160" />
|
||||||
<AndroidXmlCodeStyleSettings>
|
<AndroidXmlCodeStyleSettings>
|
||||||
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
|
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
|
||||||
</AndroidXmlCodeStyleSettings>
|
</AndroidXmlCodeStyleSettings>
|
||||||
|
@ -5,7 +5,8 @@ Features ✨:
|
|||||||
-
|
-
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
-
|
- Improve navigation to the timeline (#789, #862)
|
||||||
|
- Improve network detection. It is now based on the sync request status (#873, #882)
|
||||||
|
|
||||||
Other changes:
|
Other changes:
|
||||||
-
|
-
|
||||||
|
12
build.gradle
12
build.gradle
@ -47,23 +47,11 @@ allprojects {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(JavaCompile).all {
|
|
||||||
options.compilerArgs += [
|
|
||||||
'-Adagger.gradle.incremental=enabled'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||||
// Warnings are potential errors, so stop ignoring them
|
// Warnings are potential errors, so stop ignoring them
|
||||||
kotlinOptions.allWarningsAsErrors = true
|
kotlinOptions.allWarningsAsErrors = true
|
||||||
}
|
}
|
||||||
|
|
||||||
afterEvaluate {
|
|
||||||
extensions.findByName("kapt")?.arguments {
|
|
||||||
arg("dagger.gradle.incremental", "enabled")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
task clean(type: Delete) {
|
||||||
|
@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
|
|||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.android.MainThreadDisposable
|
import io.reactivex.android.MainThreadDisposable
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
|
||||||
private class LiveDataObservable<T>(
|
private class LiveDataObservable<T>(
|
||||||
@ -57,6 +58,14 @@ private class LiveDataObservable<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> LiveData<T>.asObservable(): Observable<T> {
|
internal fun <T> LiveData<T>.asObservable(): Observable<T> {
|
||||||
return LiveDataObservable(this).observeOn(Schedulers.computation())
|
return LiveDataObservable(this).observeOn(Schedulers.computation())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun <T> Observable<T>.startWithCallable(supplier: () -> T): Observable<T> {
|
||||||
|
val startObservable = Observable
|
||||||
|
.fromCallable(supplier)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
return startWith(startObservable)
|
||||||
|
}
|
||||||
|
@ -41,7 +41,9 @@ class RxRoom(private val room: Room, private val session: Session) {
|
|||||||
fun liveRoomSummary(): Observable<Optional<RoomSummary>> {
|
fun liveRoomSummary(): Observable<Optional<RoomSummary>> {
|
||||||
val summaryObservable = room.getRoomSummaryLive()
|
val summaryObservable = room.getRoomSummaryLive()
|
||||||
.asObservable()
|
.asObservable()
|
||||||
.startWith(room.roomSummary().toOptional())
|
.startWithCallable {
|
||||||
|
room.roomSummary().toOptional()
|
||||||
|
}
|
||||||
.doOnNext { Timber.v("RX: summary emitted for: ${it.getOrNull()?.roomId}") }
|
.doOnNext { Timber.v("RX: summary emitted for: ${it.getOrNull()?.roomId}") }
|
||||||
|
|
||||||
val memberIdsChangeObservable = summaryObservable
|
val memberIdsChangeObservable = summaryObservable
|
||||||
@ -97,7 +99,9 @@ class RxRoom(private val room: Room, private val session: Session) {
|
|||||||
|
|
||||||
fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable<List<RoomMemberSummary>> {
|
fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable<List<RoomMemberSummary>> {
|
||||||
val roomMembersObservable = room.getRoomMembersLive(queryParams).asObservable()
|
val roomMembersObservable = room.getRoomMembersLive(queryParams).asObservable()
|
||||||
.startWith(room.getRoomMembers(queryParams))
|
.startWithCallable {
|
||||||
|
room.getRoomMembers(queryParams)
|
||||||
|
}
|
||||||
.doOnNext { Timber.v("RX: room members emitted. Size: ${it.size}") }
|
.doOnNext { Timber.v("RX: room members emitted. Size: ${it.size}") }
|
||||||
|
|
||||||
// TODO Do it only for room members of the room (switchMap)
|
// TODO Do it only for room members of the room (switchMap)
|
||||||
@ -127,17 +131,23 @@ class RxRoom(private val room: Room, private val session: Session) {
|
|||||||
|
|
||||||
fun liveAnnotationSummary(eventId: String): Observable<Optional<EventAnnotationsSummary>> {
|
fun liveAnnotationSummary(eventId: String): Observable<Optional<EventAnnotationsSummary>> {
|
||||||
return room.getEventAnnotationsSummaryLive(eventId).asObservable()
|
return room.getEventAnnotationsSummaryLive(eventId).asObservable()
|
||||||
.startWith(room.getEventAnnotationsSummary(eventId).toOptional())
|
.startWithCallable {
|
||||||
|
room.getEventAnnotationsSummary(eventId).toOptional()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun liveTimelineEvent(eventId: String): Observable<Optional<TimelineEvent>> {
|
fun liveTimelineEvent(eventId: String): Observable<Optional<TimelineEvent>> {
|
||||||
return room.getTimeLineEventLive(eventId).asObservable()
|
return room.getTimeLineEventLive(eventId).asObservable()
|
||||||
.startWith(room.getTimeLineEvent(eventId).toOptional())
|
.startWithCallable {
|
||||||
|
room.getTimeLineEvent(eventId).toOptional()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun liveStateEvent(eventType: String): Observable<Optional<Event>> {
|
fun liveStateEvent(eventType: String, stateKey: String): Observable<Optional<Event>> {
|
||||||
return room.getStateEventLive(eventType).asObservable()
|
return room.getStateEventLive(eventType, stateKey).asObservable()
|
||||||
.startWith(room.getStateEvent(eventType).toOptional())
|
.startWithCallable {
|
||||||
|
room.getStateEvent(eventType, stateKey).toOptional()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun liveReadMarker(): Observable<Optional<String>> {
|
fun liveReadMarker(): Observable<Optional<String>> {
|
||||||
|
@ -40,7 +40,9 @@ class RxSession(private val session: Session) {
|
|||||||
|
|
||||||
fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
|
fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
|
||||||
val summariesObservable = session.getRoomSummariesLive(queryParams).asObservable()
|
val summariesObservable = session.getRoomSummariesLive(queryParams).asObservable()
|
||||||
.startWith(session.getRoomSummaries(queryParams))
|
.startWithCallable {
|
||||||
|
session.getRoomSummaries(queryParams)
|
||||||
|
}
|
||||||
.doOnNext { Timber.v("RX: summaries emitted: size: ${it.size}") }
|
.doOnNext { Timber.v("RX: summaries emitted: size: ${it.size}") }
|
||||||
|
|
||||||
val cryptoDeviceInfoObservable = session.getLiveCryptoDeviceInfo().asObservable()
|
val cryptoDeviceInfoObservable = session.getLiveCryptoDeviceInfo().asObservable()
|
||||||
@ -69,12 +71,16 @@ class RxSession(private val session: Session) {
|
|||||||
|
|
||||||
fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable<List<GroupSummary>> {
|
fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable<List<GroupSummary>> {
|
||||||
return session.getGroupSummariesLive(queryParams).asObservable()
|
return session.getGroupSummariesLive(queryParams).asObservable()
|
||||||
.startWith(session.getGroupSummaries(queryParams))
|
.startWithCallable {
|
||||||
|
session.getGroupSummaries(queryParams)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun liveBreadcrumbs(): Observable<List<RoomSummary>> {
|
fun liveBreadcrumbs(): Observable<List<RoomSummary>> {
|
||||||
return session.getBreadcrumbsLive().asObservable()
|
return session.getBreadcrumbsLive().asObservable()
|
||||||
.startWith(session.getBreadcrumbs())
|
.startWithCallable {
|
||||||
|
session.getBreadcrumbs()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun liveSyncState(): Observable<SyncState> {
|
fun liveSyncState(): Observable<SyncState> {
|
||||||
@ -87,7 +93,9 @@ class RxSession(private val session: Session) {
|
|||||||
|
|
||||||
fun liveUser(userId: String): Observable<Optional<User>> {
|
fun liveUser(userId: String): Observable<Optional<User>> {
|
||||||
return session.getUserLive(userId).asObservable()
|
return session.getUserLive(userId).asObservable()
|
||||||
.startWith(session.getUser(userId).toOptional())
|
.startWithCallable {
|
||||||
|
session.getUser(userId).toOptional()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun liveUsers(): Observable<List<User>> {
|
fun liveUsers(): Observable<List<User>> {
|
||||||
|
@ -92,10 +92,11 @@ dependencies {
|
|||||||
|
|
||||||
def arrow_version = "0.8.2"
|
def arrow_version = "0.8.2"
|
||||||
def moshi_version = '1.8.0'
|
def moshi_version = '1.8.0'
|
||||||
def lifecycle_version = '2.1.0'
|
def lifecycle_version = '2.2.0'
|
||||||
|
def arch_version = '2.1.0'
|
||||||
def coroutines_version = "1.3.2"
|
def coroutines_version = "1.3.2"
|
||||||
def markwon_version = '3.1.0'
|
def markwon_version = '3.1.0'
|
||||||
def daggerVersion = '2.24'
|
def daggerVersion = '2.25.4'
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||||
@ -104,14 +105,13 @@ dependencies {
|
|||||||
implementation "androidx.appcompat:appcompat:1.1.0"
|
implementation "androidx.appcompat:appcompat:1.1.0"
|
||||||
|
|
||||||
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
||||||
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||||
|
|
||||||
// Network
|
// Network
|
||||||
implementation 'com.squareup.retrofit2:retrofit:2.6.2'
|
implementation 'com.squareup.retrofit2:retrofit:2.6.2'
|
||||||
implementation 'com.squareup.retrofit2:converter-moshi:2.6.2'
|
implementation 'com.squareup.retrofit2:converter-moshi:2.6.2'
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.2.2'
|
implementation 'com.squareup.okhttp3:okhttp:4.2.2'
|
||||||
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
|
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
|
||||||
implementation 'com.novoda:merlin:1.2.0'
|
|
||||||
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
|
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
|
||||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
|
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
|
||||||
|
|
||||||
@ -125,7 +125,7 @@ dependencies {
|
|||||||
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
|
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
|
||||||
|
|
||||||
// Work
|
// Work
|
||||||
implementation "androidx.work:work-runtime-ktx:2.3.0-beta02"
|
implementation "androidx.work:work-runtime-ktx:2.3.0"
|
||||||
|
|
||||||
// FP
|
// FP
|
||||||
implementation "io.arrow-kt:arrow-core:$arrow_version"
|
implementation "io.arrow-kt:arrow-core:$arrow_version"
|
||||||
@ -167,7 +167,7 @@ dependencies {
|
|||||||
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
|
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
|
||||||
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
|
// Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
|
||||||
androidTestImplementation 'io.mockk:mockk-android:1.9.2.kotlin12'
|
androidTestImplementation 'io.mockk:mockk-android:1.9.2.kotlin12'
|
||||||
androidTestImplementation "androidx.arch.core:core-testing:$lifecycle_version"
|
androidTestImplementation "androidx.arch.core:core-testing:$arch_version"
|
||||||
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -117,6 +117,10 @@ class CommonTestHelper(context: Context) {
|
|||||||
override fun onTimelineFailure(throwable: Throwable) {
|
override fun onTimelineFailure(throwable: Throwable) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNewTimelineEvents(eventIds: List<String>) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||||
// TODO Count only new messages?
|
// TODO Count only new messages?
|
||||||
if (snapshot.count { it.root.type == EventType.MESSAGE } == nbOfMessages) {
|
if (snapshot.count { it.root.type == EventType.MESSAGE } == nbOfMessages) {
|
||||||
|
@ -241,6 +241,11 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) {
|
|||||||
|
|
||||||
val bobEventsListener = object : Timeline.Listener {
|
val bobEventsListener = object : Timeline.Listener {
|
||||||
override fun onTimelineFailure(throwable: Throwable) {
|
override fun onTimelineFailure(throwable: Throwable) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewTimelineEvents(eventIds: List<String>) {
|
||||||
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||||
|
@ -20,15 +20,15 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.InstrumentedTest
|
import im.vector.matrix.android.InstrumentedTest
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.internal.database.helper.add
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import im.vector.matrix.android.internal.database.helper.lastStateIndex
|
import im.vector.matrix.android.internal.database.helper.addTimelineEvent
|
||||||
import im.vector.matrix.android.internal.database.helper.merge
|
import im.vector.matrix.android.internal.database.helper.merge
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.toEntity
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||||
import im.vector.matrix.android.internal.database.model.SessionRealmModule
|
import im.vector.matrix.android.internal.database.model.SessionRealmModule
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||||
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents
|
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents
|
||||||
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeMessageEvent
|
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeMessageEvent
|
||||||
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeRoomMemberEvent
|
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import io.realm.kotlin.createObject
|
import io.realm.kotlin.createObject
|
||||||
@ -58,8 +58,11 @@ internal class ChunkEntityTest : InstrumentedTest {
|
|||||||
fun add_shouldAdd_whenNotAlreadyIncluded() {
|
fun add_shouldAdd_whenNotAlreadyIncluded() {
|
||||||
monarchy.runTransactionSync { realm ->
|
monarchy.runTransactionSync { realm ->
|
||||||
val chunk: ChunkEntity = realm.createObject()
|
val chunk: ChunkEntity = realm.createObject()
|
||||||
val fakeEvent = createFakeMessageEvent()
|
|
||||||
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
|
val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED).let {
|
||||||
|
realm.copyToRealmOrUpdate(it)
|
||||||
|
}
|
||||||
|
chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
|
||||||
chunk.timelineEvents.size shouldEqual 1
|
chunk.timelineEvents.size shouldEqual 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,65 +71,23 @@ internal class ChunkEntityTest : InstrumentedTest {
|
|||||||
fun add_shouldNotAdd_whenAlreadyIncluded() {
|
fun add_shouldNotAdd_whenAlreadyIncluded() {
|
||||||
monarchy.runTransactionSync { realm ->
|
monarchy.runTransactionSync { realm ->
|
||||||
val chunk: ChunkEntity = realm.createObject()
|
val chunk: ChunkEntity = realm.createObject()
|
||||||
val fakeEvent = createFakeMessageEvent()
|
val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED).let {
|
||||||
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
|
realm.copyToRealmOrUpdate(it)
|
||||||
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
|
}
|
||||||
|
chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
|
||||||
|
chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
|
||||||
chunk.timelineEvents.size shouldEqual 1
|
chunk.timelineEvents.size shouldEqual 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun add_shouldStateIndexIncremented_whenStateEventIsAddedForward() {
|
|
||||||
monarchy.runTransactionSync { realm ->
|
|
||||||
val chunk: ChunkEntity = realm.createObject()
|
|
||||||
val fakeEvent = createFakeRoomMemberEvent()
|
|
||||||
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
|
|
||||||
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun add_shouldStateIndexNotIncremented_whenNoStateEventIsAdded() {
|
|
||||||
monarchy.runTransactionSync { realm ->
|
|
||||||
val chunk: ChunkEntity = realm.createObject()
|
|
||||||
val fakeEvent = createFakeMessageEvent()
|
|
||||||
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
|
|
||||||
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun addAll_shouldStateIndexIncremented_whenStateEventsAreAddedForward() {
|
|
||||||
monarchy.runTransactionSync { realm ->
|
|
||||||
val chunk: ChunkEntity = realm.createObject()
|
|
||||||
val fakeEvents = createFakeListOfEvents(30)
|
|
||||||
val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size
|
|
||||||
chunk.addAll("roomId", fakeEvents, PaginationDirection.FORWARDS)
|
|
||||||
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual numberOfStateEvents
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun addAll_shouldStateIndexDecremented_whenStateEventsAreAddedBackward() {
|
|
||||||
monarchy.runTransactionSync { realm ->
|
|
||||||
val chunk: ChunkEntity = realm.createObject()
|
|
||||||
val fakeEvents = createFakeListOfEvents(30)
|
|
||||||
val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size
|
|
||||||
val lastIsState = fakeEvents.last().isStateEvent()
|
|
||||||
val expectedStateIndex = if (lastIsState) -numberOfStateEvents + 1 else -numberOfStateEvents
|
|
||||||
chunk.addAll("roomId", fakeEvents, PaginationDirection.BACKWARDS)
|
|
||||||
chunk.lastStateIndex(PaginationDirection.BACKWARDS) shouldEqual expectedStateIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun merge_shouldAddEvents_whenMergingBackward() {
|
fun merge_shouldAddEvents_whenMergingBackward() {
|
||||||
monarchy.runTransactionSync { realm ->
|
monarchy.runTransactionSync { realm ->
|
||||||
val chunk1: ChunkEntity = realm.createObject()
|
val chunk1: ChunkEntity = realm.createObject()
|
||||||
val chunk2: ChunkEntity = realm.createObject()
|
val chunk2: ChunkEntity = realm.createObject()
|
||||||
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
|
chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
|
||||||
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
|
chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
|
||||||
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
|
chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS)
|
||||||
chunk1.timelineEvents.size shouldEqual 60
|
chunk1.timelineEvents.size shouldEqual 60
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,9 +101,9 @@ internal class ChunkEntityTest : InstrumentedTest {
|
|||||||
val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10)
|
val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10)
|
||||||
chunk1.isLastForward = true
|
chunk1.isLastForward = true
|
||||||
chunk2.isLastForward = false
|
chunk2.isLastForward = false
|
||||||
chunk1.addAll("roomId", eventsForChunk1, PaginationDirection.FORWARDS)
|
chunk1.addAll(ROOM_ID, eventsForChunk1, PaginationDirection.FORWARDS)
|
||||||
chunk2.addAll("roomId", eventsForChunk2, PaginationDirection.BACKWARDS)
|
chunk2.addAll(ROOM_ID, eventsForChunk2, PaginationDirection.BACKWARDS)
|
||||||
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
|
chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS)
|
||||||
chunk1.timelineEvents.size shouldEqual 40
|
chunk1.timelineEvents.size shouldEqual 40
|
||||||
chunk1.isLastForward.shouldBeTrue()
|
chunk1.isLastForward.shouldBeTrue()
|
||||||
}
|
}
|
||||||
@ -155,9 +116,9 @@ internal class ChunkEntityTest : InstrumentedTest {
|
|||||||
val chunk2: ChunkEntity = realm.createObject()
|
val chunk2: ChunkEntity = realm.createObject()
|
||||||
val prevToken = "prev_token"
|
val prevToken = "prev_token"
|
||||||
chunk1.prevToken = prevToken
|
chunk1.prevToken = prevToken
|
||||||
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
|
chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
|
||||||
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
|
chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
|
||||||
chunk1.merge("roomId", chunk2, PaginationDirection.FORWARDS)
|
chunk1.merge(ROOM_ID, chunk2, PaginationDirection.FORWARDS)
|
||||||
chunk1.prevToken shouldEqual prevToken
|
chunk1.prevToken shouldEqual prevToken
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -169,19 +130,25 @@ internal class ChunkEntityTest : InstrumentedTest {
|
|||||||
val chunk2: ChunkEntity = realm.createObject()
|
val chunk2: ChunkEntity = realm.createObject()
|
||||||
val nextToken = "next_token"
|
val nextToken = "next_token"
|
||||||
chunk1.nextToken = nextToken
|
chunk1.nextToken = nextToken
|
||||||
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
|
chunk1.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
|
||||||
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
|
chunk2.addAll(ROOM_ID, createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
|
||||||
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
|
chunk1.merge(ROOM_ID, chunk2, PaginationDirection.BACKWARDS)
|
||||||
chunk1.nextToken shouldEqual nextToken
|
chunk1.nextToken shouldEqual nextToken
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ChunkEntity.addAll(roomId: String,
|
private fun ChunkEntity.addAll(roomId: String,
|
||||||
events: List<Event>,
|
events: List<Event>,
|
||||||
direction: PaginationDirection,
|
direction: PaginationDirection) {
|
||||||
stateIndexOffset: Int = 0) {
|
|
||||||
events.forEach { event ->
|
events.forEach { event ->
|
||||||
add(roomId, event, direction, stateIndexOffset)
|
val fakeEvent = event.toEntity(roomId, SendState.SYNCED).let {
|
||||||
|
realm.copyToRealmOrUpdate(it)
|
||||||
}
|
}
|
||||||
|
addTimelineEvent(roomId, fakeEvent, direction, emptyMap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ROOM_ID = "roomId"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,3 +38,8 @@ interface MatrixCallback<in T> {
|
|||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic no op implementation
|
||||||
|
*/
|
||||||
|
class NoOpMatrixCallback<T>: MatrixCallback<T>
|
||||||
|
@ -59,7 +59,7 @@ class RoomMemberCountCondition(val iz: String) : Condition(Kind.room_member_coun
|
|||||||
val (prefix, count) = match.destructured
|
val (prefix, count) = match.destructured
|
||||||
return prefix to count.toInt()
|
return prefix to count.toInt()
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Timber.d(t)
|
Timber.e(t, "Unable to parse 'is' field")
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -200,7 +200,7 @@ data class Event(
|
|||||||
|
|
||||||
fun Event.isTextMessage(): Boolean {
|
fun Event.isTextMessage(): Boolean {
|
||||||
return getClearType() == EventType.MESSAGE
|
return getClearType() == EventType.MESSAGE
|
||||||
&& when (getClearContent()?.toModel<MessageContent>()?.type) {
|
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
|
||||||
MessageType.MSGTYPE_TEXT,
|
MessageType.MSGTYPE_TEXT,
|
||||||
MessageType.MSGTYPE_EMOTE,
|
MessageType.MSGTYPE_EMOTE,
|
||||||
MessageType.MSGTYPE_NOTICE -> true
|
MessageType.MSGTYPE_NOTICE -> true
|
||||||
@ -210,7 +210,7 @@ fun Event.isTextMessage(): Boolean {
|
|||||||
|
|
||||||
fun Event.isImageMessage(): Boolean {
|
fun Event.isImageMessage(): Boolean {
|
||||||
return getClearType() == EventType.MESSAGE
|
return getClearType() == EventType.MESSAGE
|
||||||
&& when (getClearContent()?.toModel<MessageContent>()?.type) {
|
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
|
||||||
MessageType.MSGTYPE_IMAGE -> true
|
MessageType.MSGTYPE_IMAGE -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
@ -25,9 +25,9 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
|||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageAudioContent(
|
data class MessageAudioContent(
|
||||||
/**
|
/**
|
||||||
* Not documented
|
* Required. Must be 'm.audio'.
|
||||||
*/
|
*/
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val msgType: String,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Required. A description of the audio e.g. 'Bee Gees - Stayin' Alive', or some kind of content description for accessibility e.g. 'audio attachment'.
|
* Required. A description of the audio e.g. 'Bee Gees - Stayin' Alive', or some kind of content description for accessibility e.g. 'audio attachment'.
|
||||||
@ -40,7 +40,7 @@ data class MessageAudioContent(
|
|||||||
@Json(name = "info") val audioInfo: AudioInfo? = null,
|
@Json(name = "info") val audioInfo: AudioInfo? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Required. Required if the file is not encrypted. The URL (typically MXC URI) to the audio clip.
|
* Required if the file is not encrypted. The URL (typically MXC URI) to the audio clip.
|
||||||
*/
|
*/
|
||||||
@Json(name = "url") override val url: String? = null,
|
@Json(name = "url") override val url: String? = null,
|
||||||
|
|
||||||
|
@ -20,8 +20,7 @@ import im.vector.matrix.android.api.session.events.model.Content
|
|||||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||||
|
|
||||||
interface MessageContent {
|
interface MessageContent {
|
||||||
// TODO Rename to msgType
|
val msgType: String
|
||||||
val type: String
|
|
||||||
val body: String
|
val body: String
|
||||||
val relatesTo: RelationDefaultContent?
|
val relatesTo: RelationDefaultContent?
|
||||||
val newContent: Content?
|
val newContent: Content?
|
||||||
|
@ -23,7 +23,7 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC
|
|||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageDefaultContent(
|
data class MessageDefaultContent(
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val msgType: String,
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
@Json(name = "m.new_content") override val newContent: Content? = null
|
@Json(name = "m.new_content") override val newContent: Content? = null
|
||||||
|
@ -23,10 +23,26 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC
|
|||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageEmoteContent(
|
data class MessageEmoteContent(
|
||||||
@Json(name = "msgtype") override val type: String,
|
/**
|
||||||
|
* Required. Must be 'm.emote'.
|
||||||
|
*/
|
||||||
|
@Json(name = "msgtype") override val msgType: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. The emote action to perform.
|
||||||
|
*/
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The format used in the formatted_body. Currently only org.matrix.custom.html is supported.
|
||||||
|
*/
|
||||||
@Json(name = "format") val format: String? = null,
|
@Json(name = "format") val format: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The formatted version of the body. This is required if format is specified.
|
||||||
|
*/
|
||||||
@Json(name = "formatted_body") val formattedBody: String? = null,
|
@Json(name = "formatted_body") val formattedBody: String? = null,
|
||||||
|
|
||||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
@Json(name = "m.new_content") override val newContent: Content? = null
|
@Json(name = "m.new_content") override val newContent: Content? = null
|
||||||
) : MessageContent
|
) : MessageContent
|
||||||
|
@ -23,10 +23,13 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
|||||||
*/
|
*/
|
||||||
interface MessageEncryptedContent : MessageContent {
|
interface MessageEncryptedContent : MessageContent {
|
||||||
/**
|
/**
|
||||||
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image.
|
* Required if the file is unencrypted. The URL (typically MXC URI) to the image.
|
||||||
*/
|
*/
|
||||||
val url: String?
|
val url: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||||
|
*/
|
||||||
val encryptedFileInfo: EncryptedFileInfo?
|
val encryptedFileInfo: EncryptedFileInfo?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,9 +26,9 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
|||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageFileContent(
|
data class MessageFileContent(
|
||||||
/**
|
/**
|
||||||
* Not documented
|
* Required. Must be 'm.file'.
|
||||||
*/
|
*/
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val msgType: String,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Required. A human-readable description of the file. This is recommended to be the filename of the original upload.
|
* Required. A human-readable description of the file. This is recommended to be the filename of the original upload.
|
||||||
@ -46,13 +46,16 @@ data class MessageFileContent(
|
|||||||
@Json(name = "info") val info: FileInfo? = null,
|
@Json(name = "info") val info: FileInfo? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the file.
|
* Required if the file is unencrypted. The URL (typically MXC URI) to the file.
|
||||||
*/
|
*/
|
||||||
@Json(name = "url") override val url: String? = null,
|
@Json(name = "url") override val url: String? = null,
|
||||||
|
|
||||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||||
|
*/
|
||||||
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||||
) : MessageEncryptedContent {
|
) : MessageEncryptedContent {
|
||||||
|
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.api.session.room.model.message
|
||||||
|
|
||||||
|
object MessageFormat {
|
||||||
|
const val FORMAT_MATRIX_HTML = "org.matrix.custom.html"
|
||||||
|
}
|
@ -27,7 +27,7 @@ data class MessageImageContent(
|
|||||||
/**
|
/**
|
||||||
* Required. Must be 'm.image'.
|
* Required. Must be 'm.image'.
|
||||||
*/
|
*/
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val msgType: String,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Required. A textual representation of the image. This could be the alt text of the image, the filename of the image,
|
* Required. A textual representation of the image. This could be the alt text of the image, the filename of the image,
|
||||||
@ -41,7 +41,7 @@ data class MessageImageContent(
|
|||||||
@Json(name = "info") override val info: ImageInfo? = null,
|
@Json(name = "info") override val info: ImageInfo? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image.
|
* Required if the file is unencrypted. The URL (typically MXC URI) to the image.
|
||||||
*/
|
*/
|
||||||
@Json(name = "url") override val url: String? = null,
|
@Json(name = "url") override val url: String? = null,
|
||||||
|
|
||||||
|
@ -24,9 +24,9 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC
|
|||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageLocationContent(
|
data class MessageLocationContent(
|
||||||
/**
|
/**
|
||||||
* Not documented
|
* Required. Must be 'm.location'.
|
||||||
*/
|
*/
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val msgType: String,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Required. A description of the location e.g. 'Big Ben, London, UK', or some kind of content description for accessibility e.g. 'location attachment'.
|
* Required. A description of the location e.g. 'Big Ben, London, UK', or some kind of content description for accessibility e.g. 'location attachment'.
|
||||||
|
@ -23,10 +23,26 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC
|
|||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageNoticeContent(
|
data class MessageNoticeContent(
|
||||||
@Json(name = "msgtype") override val type: String,
|
/**
|
||||||
|
* Required. Must be 'm.notice'.
|
||||||
|
*/
|
||||||
|
@Json(name = "msgtype") override val msgType: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. The notice text to send.
|
||||||
|
*/
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The format used in the formatted_body. Currently only org.matrix.custom.html is supported.
|
||||||
|
*/
|
||||||
@Json(name = "format") val format: String? = null,
|
@Json(name = "format") val format: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The formatted version of the body. This is required if format is specified.
|
||||||
|
*/
|
||||||
@Json(name = "formatted_body") val formattedBody: String? = null,
|
@Json(name = "formatted_body") val formattedBody: String? = null,
|
||||||
|
|
||||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
@Json(name = "m.new_content") override val newContent: Content? = null
|
@Json(name = "m.new_content") override val newContent: Content? = null
|
||||||
) : MessageContent
|
) : MessageContent
|
||||||
|
@ -12,7 +12,6 @@
|
|||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package im.vector.matrix.android.api.session.room.model.message
|
package im.vector.matrix.android.api.session.room.model.message
|
||||||
@ -28,7 +27,7 @@ data class MessageStickerContent(
|
|||||||
/**
|
/**
|
||||||
* Set in local, not from server
|
* Set in local, not from server
|
||||||
*/
|
*/
|
||||||
override val type: String = MessageType.MSGTYPE_STICKER_LOCAL,
|
override val msgType: String = MessageType.MSGTYPE_STICKER_LOCAL,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Required. A textual representation of the image. This could be the alt text of the image, the filename of the image,
|
* Required. A textual representation of the image. This could be the alt text of the image, the filename of the image,
|
||||||
@ -42,7 +41,7 @@ data class MessageStickerContent(
|
|||||||
@Json(name = "info") override val info: ImageInfo? = null,
|
@Json(name = "info") override val info: ImageInfo? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image.
|
* Required if the file is unencrypted. The URL (typically MXC URI) to the image.
|
||||||
*/
|
*/
|
||||||
@Json(name = "url") override val url: String? = null,
|
@Json(name = "url") override val url: String? = null,
|
||||||
|
|
||||||
|
@ -23,10 +23,26 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC
|
|||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageTextContent(
|
data class MessageTextContent(
|
||||||
@Json(name = "msgtype") override val type: String,
|
/**
|
||||||
|
* Required. Must be 'm.text'.
|
||||||
|
*/
|
||||||
|
@Json(name = "msgtype") override val msgType: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. The body of the message.
|
||||||
|
*/
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The format used in the formatted_body. Currently only org.matrix.custom.html is supported.
|
||||||
|
*/
|
||||||
@Json(name = "format") val format: String? = null,
|
@Json(name = "format") val format: String? = null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The formatted version of the body. This is required if format is specified.
|
||||||
|
*/
|
||||||
@Json(name = "formatted_body") val formattedBody: String? = null,
|
@Json(name = "formatted_body") val formattedBody: String? = null,
|
||||||
|
|
||||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
@Json(name = "m.new_content") override val newContent: Content? = null
|
@Json(name = "m.new_content") override val newContent: Content? = null
|
||||||
) : MessageContent
|
) : MessageContent
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
package im.vector.matrix.android.api.session.room.model.message
|
package im.vector.matrix.android.api.session.room.model.message
|
||||||
|
|
||||||
object MessageType {
|
object MessageType {
|
||||||
|
|
||||||
const val MSGTYPE_TEXT = "m.text"
|
const val MSGTYPE_TEXT = "m.text"
|
||||||
const val MSGTYPE_EMOTE = "m.emote"
|
const val MSGTYPE_EMOTE = "m.emote"
|
||||||
const val MSGTYPE_NOTICE = "m.notice"
|
const val MSGTYPE_NOTICE = "m.notice"
|
||||||
@ -27,7 +26,6 @@ object MessageType {
|
|||||||
const val MSGTYPE_LOCATION = "m.location"
|
const val MSGTYPE_LOCATION = "m.location"
|
||||||
const val MSGTYPE_FILE = "m.file"
|
const val MSGTYPE_FILE = "m.file"
|
||||||
const val MSGTYPE_VERIFICATION_REQUEST = "m.key.verification.request"
|
const val MSGTYPE_VERIFICATION_REQUEST = "m.key.verification.request"
|
||||||
const val FORMAT_MATRIX_HTML = "org.matrix.custom.html"
|
|
||||||
// Add, in local, a fake message type in order to StickerMessage can inherit Message class
|
// Add, in local, a fake message type in order to StickerMessage can inherit Message class
|
||||||
// Because sticker isn't a message type but a event type without msgtype field
|
// Because sticker isn't a message type but a event type without msgtype field
|
||||||
const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker"
|
const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker"
|
||||||
|
@ -24,7 +24,7 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationInfoReq
|
|||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MessageVerificationRequestContent(
|
data class MessageVerificationRequestContent(
|
||||||
@Json(name = "msgtype") override val type: String = MessageType.MSGTYPE_VERIFICATION_REQUEST,
|
@Json(name = "msgtype") override val msgType: String = MessageType.MSGTYPE_VERIFICATION_REQUEST,
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
@Json(name = "from_device") override val fromDevice: String?,
|
@Json(name = "from_device") override val fromDevice: String?,
|
||||||
@Json(name = "methods") override val methods: List<String>,
|
@Json(name = "methods") override val methods: List<String>,
|
||||||
|
@ -27,7 +27,7 @@ data class MessageVideoContent(
|
|||||||
/**
|
/**
|
||||||
* Required. Must be 'm.video'.
|
* Required. Must be 'm.video'.
|
||||||
*/
|
*/
|
||||||
@Json(name = "msgtype") override val type: String,
|
@Json(name = "msgtype") override val msgType: String,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Required. A description of the video e.g. 'Gangnam style', or some kind of content description for accessibility e.g. 'video attachment'.
|
* Required. A description of the video e.g. 'Gangnam style', or some kind of content description for accessibility e.g. 'video attachment'.
|
||||||
@ -40,7 +40,7 @@ data class MessageVideoContent(
|
|||||||
@Json(name = "info") val videoInfo: VideoInfo? = null,
|
@Json(name = "info") val videoInfo: VideoInfo? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the video clip.
|
* Required if the file is unencrypted. The URL (typically MXC URI) to the video clip.
|
||||||
*/
|
*/
|
||||||
@Json(name = "url") override val url: String? = null,
|
@Json(name = "url") override val url: String? = null,
|
||||||
|
|
||||||
|
@ -26,10 +26,16 @@ import im.vector.matrix.android.api.util.Optional
|
|||||||
*/
|
*/
|
||||||
interface ReadService {
|
interface ReadService {
|
||||||
|
|
||||||
|
enum class MarkAsReadParams {
|
||||||
|
READ_RECEIPT,
|
||||||
|
READ_MARKER,
|
||||||
|
BOTH
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force the read marker to be set on the latest event.
|
* Force the read marker to be set on the latest event.
|
||||||
*/
|
*/
|
||||||
fun markAllAsRead(callback: MatrixCallback<Unit>)
|
fun markAsRead(params: MarkAsReadParams = MarkAsReadParams.BOTH, callback: MatrixCallback<Unit>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the read receipt on the event with provided eventId.
|
* Set the read receipt on the event with provided eventId.
|
||||||
|
@ -17,18 +17,20 @@
|
|||||||
package im.vector.matrix.android.api.session.room.send
|
package im.vector.matrix.android.api.session.room.send
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
|
|
||||||
interface DraftService {
|
interface DraftService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save or update a draft to the room
|
* Save or update a draft to the room
|
||||||
*/
|
*/
|
||||||
fun saveDraft(draft: UserDraft)
|
fun saveDraft(draft: UserDraft, callback: MatrixCallback<Unit>): Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete the last draft, basically just after sending the message
|
* Delete the last draft, basically just after sending the message
|
||||||
*/
|
*/
|
||||||
fun deleteDraft()
|
fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the current drafts if any, as a live data
|
* Return the current drafts if any, as a live data
|
||||||
|
@ -28,7 +28,7 @@ interface StateService {
|
|||||||
*/
|
*/
|
||||||
fun updateTopic(topic: String, callback: MatrixCallback<Unit>)
|
fun updateTopic(topic: String, callback: MatrixCallback<Unit>)
|
||||||
|
|
||||||
fun getStateEvent(eventType: String): Event?
|
fun getStateEvent(eventType: String, stateKey: String): Event?
|
||||||
|
|
||||||
fun getStateEventLive(eventType: String): LiveData<Optional<Event>>
|
fun getStateEventLive(eventType: String, stateKey: String): LiveData<Optional<Event>>
|
||||||
}
|
}
|
||||||
|
@ -112,6 +112,11 @@ interface Timeline {
|
|||||||
* Called whenever an error we can't recover from occurred
|
* Called whenever an error we can't recover from occurred
|
||||||
*/
|
*/
|
||||||
fun onTimelineFailure(throwable: Throwable)
|
fun onTimelineFailure(throwable: Throwable)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when new events come through the sync
|
||||||
|
*/
|
||||||
|
fun onNewTimelineEvents(eventIds: List<String>)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventConten
|
|||||||
data class TimelineEvent(
|
data class TimelineEvent(
|
||||||
val root: Event,
|
val root: Event,
|
||||||
val localId: Long,
|
val localId: Long,
|
||||||
|
val eventId: String,
|
||||||
val displayIndex: Int,
|
val displayIndex: Int,
|
||||||
val senderName: String?,
|
val senderName: String?,
|
||||||
val isUniqueDisplayName: Boolean,
|
val isUniqueDisplayName: Boolean,
|
||||||
|
@ -26,6 +26,7 @@ import com.squareup.moshi.Types
|
|||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.NoOpMatrixCallback
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
import im.vector.matrix.android.api.crypto.MXCryptoConfig
|
import im.vector.matrix.android.api.crypto.MXCryptoConfig
|
||||||
import im.vector.matrix.android.api.failure.Failure
|
import im.vector.matrix.android.api.failure.Failure
|
||||||
@ -71,7 +72,7 @@ import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
|
|||||||
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
|
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.whereType
|
||||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
import im.vector.matrix.android.internal.extensions.foldToCallback
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
@ -190,7 +191,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
this.callback = object : MatrixCallback<Unit> {
|
this.callback = object : MatrixCallback<Unit> {
|
||||||
override fun onSuccess(data: Unit) {
|
override fun onSuccess(data: Unit) {
|
||||||
// bg refresh of crypto device
|
// bg refresh of crypto device
|
||||||
downloadKeys(listOf(credentials.userId), true, object : MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {})
|
downloadKeys(listOf(credentials.userId), true, NoOpMatrixCallback())
|
||||||
callback.onSuccess(data)
|
callback.onSuccess(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -533,7 +534,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
override fun isRoomEncrypted(roomId: String): Boolean {
|
override fun isRoomEncrypted(roomId: String): Boolean {
|
||||||
val encryptionEvent = monarchy.fetchCopied { realm ->
|
val encryptionEvent = monarchy.fetchCopied { realm ->
|
||||||
EventEntity.where(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
|
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
|
||||||
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
|
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
|
||||||
.findFirst()
|
.findFirst()
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,6 @@ import im.vector.matrix.android.internal.crypto.tasks.UploadSignaturesTask
|
|||||||
import im.vector.matrix.android.internal.crypto.tasks.UploadSigningKeysTask
|
import im.vector.matrix.android.internal.crypto.tasks.UploadSigningKeysTask
|
||||||
import im.vector.matrix.android.internal.di.UserId
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
import im.vector.matrix.android.internal.task.TaskConstraints
|
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||||
@ -211,7 +210,6 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey?.toBase64NoPadding(), uskPrivateKey?.toBase64NoPadding(), sskPrivateKey?.toBase64NoPadding())
|
cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey?.toBase64NoPadding(), uskPrivateKey?.toBase64NoPadding(), sskPrivateKey?.toBase64NoPadding())
|
||||||
|
|
||||||
uploadSigningKeysTask.configureWith(params) {
|
uploadSigningKeysTask.configureWith(params) {
|
||||||
this.constraints = TaskConstraints(true)
|
|
||||||
this.callback = object : MatrixCallback<Unit> {
|
this.callback = object : MatrixCallback<Unit> {
|
||||||
override fun onSuccess(data: Unit) {
|
override fun onSuccess(data: Unit) {
|
||||||
Timber.i("## CrossSigning - Keys successfully uploaded")
|
Timber.i("## CrossSigning - Keys successfully uploaded")
|
||||||
@ -247,7 +245,6 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
resetTrustOnKeyChange()
|
resetTrustOnKeyChange()
|
||||||
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) {
|
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) {
|
||||||
// this.retryCount = 3
|
// this.retryCount = 3
|
||||||
this.constraints = TaskConstraints(true)
|
|
||||||
this.callback = object : MatrixCallback<Unit> {
|
this.callback = object : MatrixCallback<Unit> {
|
||||||
override fun onSuccess(data: Unit) {
|
override fun onSuccess(data: Unit) {
|
||||||
Timber.i("## CrossSigning - signatures successfully uploaded")
|
Timber.i("## CrossSigning - signatures successfully uploaded")
|
||||||
@ -396,7 +393,7 @@ internal class DefaultCrossSigningService @Inject constructor(
|
|||||||
return@forEach
|
return@forEach
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
// log
|
// log
|
||||||
Timber.v(failure)
|
Timber.w(failure, "Signature not valid?")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -414,7 +414,7 @@ internal class KeysBackup @Inject constructor(
|
|||||||
olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySignature)
|
olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySignature)
|
||||||
isSignatureValid = true
|
isSignatureValid = true
|
||||||
} catch (e: OlmException) {
|
} catch (e: OlmException) {
|
||||||
Timber.v(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}")
|
Timber.w(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
|||||||
// done from another device of mine
|
// done from another device of mine
|
||||||
|
|
||||||
if (EventType.MESSAGE == event.type) {
|
if (EventType.MESSAGE == event.type) {
|
||||||
val msgType = event.getClearContent().toModel<MessageContent>()?.type
|
val msgType = event.getClearContent().toModel<MessageContent>()?.msgType
|
||||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
|
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
|
||||||
event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
|
event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
|
||||||
if (it.fromDevice != deviceId) {
|
if (it.fromDevice != deviceId) {
|
||||||
@ -144,7 +144,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
|||||||
params.verificationService.onRoomEvent(event)
|
params.verificationService.onRoomEvent(event)
|
||||||
}
|
}
|
||||||
EventType.MESSAGE -> {
|
EventType.MESSAGE -> {
|
||||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
|
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.msgType) {
|
||||||
params.verificationService.onRoomRequestReceived(event)
|
params.verificationService.onRoomRequestReceived(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,9 @@ internal class DefaultUploadSignaturesTask @Inject constructor(
|
|||||||
override suspend fun execute(params: UploadSignaturesTask.Params) {
|
override suspend fun execute(params: UploadSignaturesTask.Params) {
|
||||||
try {
|
try {
|
||||||
val response = executeRequest<SignatureUploadResponse>(eventBus) {
|
val response = executeRequest<SignatureUploadResponse>(eventBus) {
|
||||||
apiCall = cryptoApi.uploadSignatures(params.signatures)
|
this.isRetryable = true
|
||||||
|
this.maxRetryCount = 10
|
||||||
|
this.apiCall = cryptoApi.uploadSignatures(params.signatures)
|
||||||
}
|
}
|
||||||
if (response.failures?.isNotEmpty() == true) {
|
if (response.failures?.isNotEmpty() == true) {
|
||||||
throw Throwable(response.failures.toString())
|
throw Throwable(response.failures.toString())
|
||||||
|
@ -165,7 +165,7 @@ internal class DefaultVerificationService @Inject constructor(
|
|||||||
onRoomDoneReceived(event)
|
onRoomDoneReceived(event)
|
||||||
}
|
}
|
||||||
EventType.MESSAGE -> {
|
EventType.MESSAGE -> {
|
||||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.type) {
|
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.msgType) {
|
||||||
onRoomRequestReceived(event)
|
onRoomRequestReceived(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ import im.vector.matrix.android.internal.crypto.tasks.RoomVerificationUpdateTask
|
|||||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.query.types
|
import im.vector.matrix.android.internal.database.query.whereTypes
|
||||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
@ -42,7 +42,7 @@ internal class VerificationMessageLiveObserver @Inject constructor(
|
|||||||
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
||||||
|
|
||||||
override val query = Monarchy.Query {
|
override val query = Monarchy.Query {
|
||||||
EventEntity.types(it, listOf(
|
EventEntity.whereTypes(it, listOf(
|
||||||
EventType.KEY_VERIFICATION_START,
|
EventType.KEY_VERIFICATION_START,
|
||||||
EventType.KEY_VERIFICATION_ACCEPT,
|
EventType.KEY_VERIFICATION_ACCEPT,
|
||||||
EventType.KEY_VERIFICATION_KEY,
|
EventType.KEY_VERIFICATION_KEY,
|
||||||
|
@ -331,7 +331,7 @@ internal class VerificationTransportRoomMessage(
|
|||||||
content = content,
|
content = content,
|
||||||
unsignedData = UnsignedData(age = null, transactionId = localID)
|
unsignedData = UnsignedData(age = null, transactionId = localID)
|
||||||
).also {
|
).also {
|
||||||
localEchoEventFactory.saveLocalEcho(monarchy, it)
|
localEchoEventFactory.createLocalEcho(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,19 +16,27 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.database.helper
|
package im.vector.matrix.android.internal.database.helper
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.room.model.RoomMemberContent
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.mapper.toEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.*
|
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||||
|
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
|
||||||
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
|
||||||
import im.vector.matrix.android.internal.database.query.find
|
import im.vector.matrix.android.internal.database.query.find
|
||||||
import im.vector.matrix.android.internal.database.query.getOrCreate
|
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.extensions.assertIsManaged
|
import im.vector.matrix.android.internal.extensions.assertIsManaged
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||||
|
import io.realm.Realm
|
||||||
import io.realm.Sort
|
import io.realm.Sort
|
||||||
import io.realm.kotlin.createObject
|
import io.realm.kotlin.createObject
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
internal fun ChunkEntity.deleteOnCascade() {
|
internal fun ChunkEntity.deleteOnCascade() {
|
||||||
assertIsManaged()
|
assertIsManaged()
|
||||||
@ -36,116 +44,154 @@ internal fun ChunkEntity.deleteOnCascade() {
|
|||||||
this.deleteFromRealm()
|
this.deleteFromRealm()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun ChunkEntity.merge(roomId: String,
|
internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direction: PaginationDirection) {
|
||||||
chunkToMerge: ChunkEntity,
|
|
||||||
direction: PaginationDirection): List<TimelineEventEntity> {
|
|
||||||
assertIsManaged()
|
assertIsManaged()
|
||||||
val isChunkToMergeUnlinked = chunkToMerge.isUnlinked
|
val localRealm = this.realm
|
||||||
val isCurrentChunkUnlinked = isUnlinked
|
|
||||||
|
|
||||||
if (isCurrentChunkUnlinked && !isChunkToMergeUnlinked) {
|
|
||||||
this.timelineEvents.forEach { it.root?.isUnlinked = false }
|
|
||||||
}
|
|
||||||
val eventsToMerge: List<TimelineEventEntity>
|
val eventsToMerge: List<TimelineEventEntity>
|
||||||
if (direction == PaginationDirection.FORWARDS) {
|
if (direction == PaginationDirection.FORWARDS) {
|
||||||
this.nextToken = chunkToMerge.nextToken
|
this.nextToken = chunkToMerge.nextToken
|
||||||
this.isLastForward = chunkToMerge.isLastForward
|
this.isLastForward = chunkToMerge.isLastForward
|
||||||
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING)
|
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
|
||||||
} else {
|
} else {
|
||||||
this.prevToken = chunkToMerge.prevToken
|
this.prevToken = chunkToMerge.prevToken
|
||||||
this.isLastBackward = chunkToMerge.isLastBackward
|
this.isLastBackward = chunkToMerge.isLastBackward
|
||||||
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
|
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
|
}
|
||||||
|
chunkToMerge.stateEvents.forEach { stateEvent ->
|
||||||
|
addStateEvent(roomId, stateEvent, direction)
|
||||||
}
|
}
|
||||||
return eventsToMerge
|
return eventsToMerge
|
||||||
.mapNotNull {
|
.forEach {
|
||||||
val event = it.root?.asDomain() ?: return@mapNotNull null
|
addTimelineEventFromMerge(localRealm, it, direction)
|
||||||
add(roomId, event, direction)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun ChunkEntity.add(roomId: String,
|
internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, direction: PaginationDirection) {
|
||||||
event: Event,
|
if (direction == PaginationDirection.BACKWARDS) {
|
||||||
direction: PaginationDirection,
|
Timber.v("We don't keep chunk state events when paginating backward")
|
||||||
stateIndexOffset: Int = 0
|
|
||||||
): TimelineEventEntity? {
|
|
||||||
assertIsManaged()
|
|
||||||
if (event.eventId != null && timelineEvents.find(event.eventId) != null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
var currentDisplayIndex = lastDisplayIndex(direction, 0)
|
|
||||||
if (direction == PaginationDirection.FORWARDS) {
|
|
||||||
currentDisplayIndex += 1
|
|
||||||
forwardsDisplayIndex = currentDisplayIndex
|
|
||||||
} else {
|
} else {
|
||||||
currentDisplayIndex -= 1
|
val stateKey = stateEvent.stateKey ?: return
|
||||||
backwardsDisplayIndex = currentDisplayIndex
|
val type = stateEvent.type
|
||||||
}
|
val pastStateEvent = stateEvents.where()
|
||||||
var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset)
|
.equalTo(EventEntityFields.ROOM_ID, roomId)
|
||||||
if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(event.type)) {
|
.equalTo(EventEntityFields.STATE_KEY, stateKey)
|
||||||
currentStateIndex += 1
|
.equalTo(CurrentStateEventEntityFields.TYPE, type)
|
||||||
forwardsStateIndex = currentStateIndex
|
.findFirst()
|
||||||
} else if (direction == PaginationDirection.BACKWARDS && timelineEvents.isNotEmpty()) {
|
|
||||||
val lastEventType = timelineEvents.last()?.root?.type ?: ""
|
|
||||||
if (EventType.isStateEvent(lastEventType)) {
|
|
||||||
currentStateIndex -= 1
|
|
||||||
backwardsStateIndex = currentStateIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val isChunkUnlinked = isUnlinked
|
if (pastStateEvent != null) {
|
||||||
|
stateEvents.remove(pastStateEvent)
|
||||||
|
}
|
||||||
|
stateEvents.add(stateEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun ChunkEntity.addTimelineEvent(roomId: String,
|
||||||
|
eventEntity: EventEntity,
|
||||||
|
direction: PaginationDirection,
|
||||||
|
roomMemberContentsByUser: Map<String, RoomMemberContent?>) {
|
||||||
|
val eventId = eventEntity.eventId
|
||||||
|
if (timelineEvents.find(eventId) != null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val displayIndex = nextDisplayIndex(direction)
|
||||||
val localId = TimelineEventEntity.nextId(realm)
|
val localId = TimelineEventEntity.nextId(realm)
|
||||||
val eventId = event.eventId ?: ""
|
val senderId = eventEntity.sender ?: ""
|
||||||
val senderId = event.senderId ?: ""
|
|
||||||
|
|
||||||
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
|
|
||||||
?: realm.createObject<ReadReceiptsSummaryEntity>(eventId).apply {
|
|
||||||
this.roomId = roomId
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update RR for the sender of a new message with a dummy one
|
// Update RR for the sender of a new message with a dummy one
|
||||||
|
val readReceiptsSummaryEntity = handleReadReceipts(realm, roomId, eventEntity, senderId)
|
||||||
|
val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
|
||||||
|
this.localId = localId
|
||||||
|
this.root = eventEntity
|
||||||
|
this.eventId = eventId
|
||||||
|
this.roomId = roomId
|
||||||
|
this.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||||
|
this.readReceipts = readReceiptsSummaryEntity
|
||||||
|
this.displayIndex = displayIndex
|
||||||
|
val roomMemberContent = roomMemberContentsByUser[senderId]
|
||||||
|
this.senderAvatar = roomMemberContent?.avatarUrl
|
||||||
|
this.senderName = roomMemberContent?.displayName
|
||||||
|
isUniqueDisplayName = if (roomMemberContent?.displayName != null) {
|
||||||
|
computeIsUnique(realm, roomId, isLastForward, roomMemberContent, roomMemberContentsByUser)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timelineEvents.add(timelineEventEntity)
|
||||||
|
}
|
||||||
|
|
||||||
if (event.originServerTs != null) {
|
private fun computeIsUnique(
|
||||||
val timestampOfEvent = event.originServerTs.toDouble()
|
realm: Realm,
|
||||||
|
roomId: String,
|
||||||
|
isLastForward: Boolean,
|
||||||
|
myRoomMemberContent: RoomMemberContent,
|
||||||
|
roomMemberContentsByUser: Map<String, RoomMemberContent?>
|
||||||
|
): Boolean {
|
||||||
|
val isHistoricalUnique = roomMemberContentsByUser.values.find {
|
||||||
|
it != myRoomMemberContent && it?.displayName == myRoomMemberContent.displayName
|
||||||
|
} == null
|
||||||
|
return if (isLastForward) {
|
||||||
|
val isLiveUnique = RoomMemberSummaryEntity
|
||||||
|
.where(realm, roomId)
|
||||||
|
.equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, myRoomMemberContent.displayName)
|
||||||
|
.findAll().none {
|
||||||
|
!roomMemberContentsByUser.containsKey(it.userId)
|
||||||
|
}
|
||||||
|
isHistoricalUnique && isLiveUnique
|
||||||
|
} else {
|
||||||
|
isHistoricalUnique
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ChunkEntity.addTimelineEventFromMerge(realm: Realm, timelineEventEntity: TimelineEventEntity, direction: PaginationDirection) {
|
||||||
|
val eventId = timelineEventEntity.eventId
|
||||||
|
if (timelineEvents.find(eventId) != null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val displayIndex = nextDisplayIndex(direction)
|
||||||
|
val localId = TimelineEventEntity.nextId(realm)
|
||||||
|
val copied = realm.createObject<TimelineEventEntity>().apply {
|
||||||
|
this.localId = localId
|
||||||
|
this.root = timelineEventEntity.root
|
||||||
|
this.eventId = timelineEventEntity.eventId
|
||||||
|
this.roomId = timelineEventEntity.roomId
|
||||||
|
this.annotations = timelineEventEntity.annotations
|
||||||
|
this.readReceipts = timelineEventEntity.readReceipts
|
||||||
|
this.displayIndex = displayIndex
|
||||||
|
this.senderAvatar = timelineEventEntity.senderAvatar
|
||||||
|
this.senderName = timelineEventEntity.senderName
|
||||||
|
this.isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName
|
||||||
|
}
|
||||||
|
timelineEvents.add(copied)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity {
|
||||||
|
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst()
|
||||||
|
?: realm.createObject<ReadReceiptsSummaryEntity>(eventEntity.eventId).apply {
|
||||||
|
this.roomId = roomId
|
||||||
|
}
|
||||||
|
val originServerTs = eventEntity.originServerTs
|
||||||
|
if (originServerTs != null) {
|
||||||
|
val timestampOfEvent = originServerTs.toDouble()
|
||||||
val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId)
|
val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId)
|
||||||
// If the synced RR is older, update
|
// If the synced RR is older, update
|
||||||
if (timestampOfEvent > readReceiptOfSender.originServerTs) {
|
if (timestampOfEvent > readReceiptOfSender.originServerTs) {
|
||||||
val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst()
|
val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst()
|
||||||
readReceiptOfSender.eventId = eventId
|
readReceiptOfSender.eventId = eventEntity.eventId
|
||||||
readReceiptOfSender.originServerTs = timestampOfEvent
|
readReceiptOfSender.originServerTs = timestampOfEvent
|
||||||
previousReceiptsSummary?.readReceipts?.remove(readReceiptOfSender)
|
previousReceiptsSummary?.readReceipts?.remove(readReceiptOfSender)
|
||||||
readReceiptsSummaryEntity.readReceipts.add(readReceiptOfSender)
|
readReceiptsSummaryEntity.readReceipts.add(readReceiptOfSender)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return readReceiptsSummaryEntity
|
||||||
val rootEvent = event.toEntity(roomId).apply {
|
|
||||||
this.stateIndex = currentStateIndex
|
|
||||||
this.displayIndex = currentDisplayIndex
|
|
||||||
this.sendState = SendState.SYNCED
|
|
||||||
this.isUnlinked = isChunkUnlinked
|
|
||||||
}
|
|
||||||
val eventEntity = realm.createObject<TimelineEventEntity>().also {
|
|
||||||
it.localId = localId
|
|
||||||
it.root = realm.copyToRealm(rootEvent)
|
|
||||||
it.eventId = eventId
|
|
||||||
it.roomId = roomId
|
|
||||||
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
|
||||||
it.readReceipts = readReceiptsSummaryEntity
|
|
||||||
}
|
|
||||||
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
|
|
||||||
timelineEvents.add(position, eventEntity)
|
|
||||||
return eventEntity
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
|
internal fun ChunkEntity.nextDisplayIndex(direction: PaginationDirection): Int {
|
||||||
return when (direction) {
|
return when (direction) {
|
||||||
PaginationDirection.FORWARDS -> forwardsDisplayIndex
|
PaginationDirection.FORWARDS -> {
|
||||||
PaginationDirection.BACKWARDS -> backwardsDisplayIndex
|
(timelineEvents.where().max(TimelineEventEntityFields.DISPLAY_INDEX)?.toInt() ?: 0) + 1
|
||||||
} ?: defaultValue
|
}
|
||||||
}
|
PaginationDirection.BACKWARDS -> {
|
||||||
|
(timelineEvents.where().min(TimelineEventEntityFields.DISPLAY_INDEX)?.toInt() ?: 0) - 1
|
||||||
internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
|
}
|
||||||
return when (direction) {
|
}
|
||||||
PaginationDirection.FORWARDS -> forwardsStateIndex
|
|
||||||
PaginationDirection.BACKWARDS -> backwardsStateIndex
|
|
||||||
} ?: defaultValue
|
|
||||||
}
|
}
|
||||||
|
@ -16,15 +16,8 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.database.helper
|
package im.vector.matrix.android.internal.database.helper
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
|
||||||
import im.vector.matrix.android.internal.database.mapper.toEntity
|
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
|
||||||
import im.vector.matrix.android.internal.database.query.fastContains
|
|
||||||
import im.vector.matrix.android.internal.extensions.assertIsManaged
|
|
||||||
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
|
|
||||||
|
|
||||||
internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) {
|
internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) {
|
||||||
chunks.remove(chunkEntity)
|
chunks.remove(chunkEntity)
|
||||||
@ -36,39 +29,3 @@ internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) {
|
|||||||
chunks.add(chunkEntity)
|
chunks.add(chunkEntity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun RoomEntity.addStateEvent(stateEvent: Event,
|
|
||||||
stateIndex: Int = Int.MIN_VALUE,
|
|
||||||
filterDuplicates: Boolean = false,
|
|
||||||
isUnlinked: Boolean = false) {
|
|
||||||
assertIsManaged()
|
|
||||||
if (stateEvent.eventId == null || (filterDuplicates && fastContains(stateEvent.eventId))) {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
val entity = stateEvent.toEntity(roomId).apply {
|
|
||||||
this.stateIndex = stateIndex
|
|
||||||
this.isUnlinked = isUnlinked
|
|
||||||
this.sendState = SendState.SYNCED
|
|
||||||
}
|
|
||||||
untimelinedStateEvents.add(entity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
internal fun RoomEntity.addSendingEvent(event: Event) {
|
|
||||||
assertIsManaged()
|
|
||||||
val senderId = event.senderId ?: return
|
|
||||||
val eventEntity = event.toEntity(roomId).apply {
|
|
||||||
this.sendState = SendState.UNSENT
|
|
||||||
}
|
|
||||||
val roomMembers = RoomMemberHelper(realm, roomId)
|
|
||||||
val myUser = roomMembers.getLastRoomMember(senderId)
|
|
||||||
val localId = TimelineEventEntity.nextId(realm)
|
|
||||||
val timelineEventEntity = TimelineEventEntity(localId).also {
|
|
||||||
it.root = eventEntity
|
|
||||||
it.eventId = event.eventId ?: ""
|
|
||||||
it.roomId = roomId
|
|
||||||
it.senderName = myUser?.displayName
|
|
||||||
it.senderAvatar = myUser?.avatarUrl
|
|
||||||
it.isUniqueDisplayName = roomMembers.isUniqueDisplayName(myUser?.displayName)
|
|
||||||
}
|
|
||||||
sendingTimelineEvents.add(0, timelineEventEntity)
|
|
||||||
}
|
|
||||||
|
@ -1,148 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.database.helper
|
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomMemberContent
|
|
||||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
|
||||||
import im.vector.matrix.android.internal.database.model.*
|
|
||||||
import im.vector.matrix.android.internal.database.query.next
|
|
||||||
import im.vector.matrix.android.internal.database.query.prev
|
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
|
||||||
import im.vector.matrix.android.internal.extensions.assertIsManaged
|
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
|
||||||
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
|
|
||||||
import io.realm.RealmList
|
|
||||||
import io.realm.RealmQuery
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is an internal cache to avoid querying all the time the room member events
|
|
||||||
*/
|
|
||||||
@SessionScope
|
|
||||||
internal class TimelineEventSenderVisitor @Inject constructor() {
|
|
||||||
|
|
||||||
internal data class Key(
|
|
||||||
val roomId: String,
|
|
||||||
val stateIndex: Int,
|
|
||||||
val senderId: String
|
|
||||||
)
|
|
||||||
|
|
||||||
internal class Value(
|
|
||||||
var senderAvatar: String? = null,
|
|
||||||
var senderName: String? = null,
|
|
||||||
var isUniqueDisplayName: Boolean = false,
|
|
||||||
var senderMembershipEventId: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
private val values = HashMap<Key, Value>()
|
|
||||||
|
|
||||||
fun clear() {
|
|
||||||
values.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clear(roomId: String, senderId: String) {
|
|
||||||
val keysToRemove = values.keys.filter { it.senderId == senderId && it.roomId == roomId }
|
|
||||||
keysToRemove.forEach {
|
|
||||||
values.remove(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun visit(timelineEventEntities: List<TimelineEventEntity>) = timelineEventEntities.forEach { visit(it) }
|
|
||||||
|
|
||||||
fun visit(timelineEventEntity: TimelineEventEntity) {
|
|
||||||
if (!timelineEventEntity.isValid) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val key = Key(
|
|
||||||
roomId = timelineEventEntity.roomId,
|
|
||||||
stateIndex = timelineEventEntity.root?.stateIndex ?: 0,
|
|
||||||
senderId = timelineEventEntity.root?.sender ?: ""
|
|
||||||
)
|
|
||||||
val result = values.getOrPut(key) {
|
|
||||||
timelineEventEntity.computeValue()
|
|
||||||
}
|
|
||||||
timelineEventEntity.apply {
|
|
||||||
this.isUniqueDisplayName = result.isUniqueDisplayName
|
|
||||||
this.senderAvatar = result.senderAvatar
|
|
||||||
this.senderName = result.senderName
|
|
||||||
this.senderMembershipEventId = result.senderMembershipEventId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun RealmList<TimelineEventEntity>.buildQuery(sender: String, isUnlinked: Boolean): RealmQuery<TimelineEventEntity> {
|
|
||||||
return where()
|
|
||||||
.equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, sender)
|
|
||||||
.equalTo(TimelineEventEntityFields.ROOT.TYPE, EventType.STATE_ROOM_MEMBER)
|
|
||||||
.equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, isUnlinked)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun TimelineEventEntity.computeValue(): Value {
|
|
||||||
assertIsManaged()
|
|
||||||
val result = Value()
|
|
||||||
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return result
|
|
||||||
val stateIndex = root?.stateIndex ?: return result
|
|
||||||
val senderId = root?.sender ?: return result
|
|
||||||
val chunkEntity = chunk?.firstOrNull() ?: return result
|
|
||||||
val isUnlinked = chunkEntity.isUnlinked
|
|
||||||
var senderMembershipEvent: EventEntity?
|
|
||||||
var senderRoomMemberContent: String?
|
|
||||||
var senderRoomMemberPrevContent: String?
|
|
||||||
|
|
||||||
if (stateIndex <= 0) {
|
|
||||||
senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).next(from = stateIndex)?.root
|
|
||||||
senderRoomMemberContent = senderMembershipEvent?.prevContent
|
|
||||||
senderRoomMemberPrevContent = senderMembershipEvent?.content
|
|
||||||
} else {
|
|
||||||
senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).prev(since = stateIndex)?.root
|
|
||||||
senderRoomMemberContent = senderMembershipEvent?.content
|
|
||||||
senderRoomMemberPrevContent = senderMembershipEvent?.prevContent
|
|
||||||
}
|
|
||||||
|
|
||||||
// We fallback to untimelinedStateEvents if we can't find membership events in timeline
|
|
||||||
if (senderMembershipEvent == null) {
|
|
||||||
senderMembershipEvent = roomEntity.untimelinedStateEvents
|
|
||||||
.where()
|
|
||||||
.equalTo(EventEntityFields.STATE_KEY, senderId)
|
|
||||||
.equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER)
|
|
||||||
.prev(since = stateIndex)
|
|
||||||
senderRoomMemberContent = senderMembershipEvent?.content
|
|
||||||
senderRoomMemberPrevContent = senderMembershipEvent?.prevContent
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentMapper.map(senderRoomMemberContent).toModel<RoomMemberContent>()?.also {
|
|
||||||
result.senderAvatar = it.avatarUrl
|
|
||||||
result.senderName = it.displayName
|
|
||||||
result.isUniqueDisplayName = RoomMemberHelper(realm, roomId).isUniqueDisplayName(it.displayName)
|
|
||||||
}
|
|
||||||
// We try to fallback on prev content if we got a room member state events with null fields
|
|
||||||
if (root?.type == EventType.STATE_ROOM_MEMBER) {
|
|
||||||
ContentMapper.map(senderRoomMemberPrevContent).toModel<RoomMemberContent>()?.also {
|
|
||||||
if (result.senderAvatar == null && it.avatarUrl != null) {
|
|
||||||
result.senderAvatar = it.avatarUrl
|
|
||||||
}
|
|
||||||
if (result.senderName == null && it.displayName != null) {
|
|
||||||
result.senderName = it.displayName
|
|
||||||
result.isUniqueDisplayName = RoomMemberHelper(realm, roomId).isUniqueDisplayName(it.displayName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.senderMembershipEventId = senderMembershipEvent?.eventId
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
@ -20,6 +20,7 @@ import com.squareup.moshi.JsonDataException
|
|||||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.events.model.UnsignedData
|
import im.vector.matrix.android.api.session.events.model.UnsignedData
|
||||||
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
@ -43,7 +44,6 @@ internal object EventMapper {
|
|||||||
eventEntity.redacts = event.redacts
|
eventEntity.redacts = event.redacts
|
||||||
eventEntity.age = event.unsignedData?.age ?: event.originServerTs
|
eventEntity.age = event.unsignedData?.age ?: event.originServerTs
|
||||||
eventEntity.unsignedData = uds
|
eventEntity.unsignedData = uds
|
||||||
eventEntity.ageLocalTs = event.ageLocalTs
|
|
||||||
return eventEntity
|
return eventEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,6 +92,9 @@ internal fun EventEntity.asDomain(): Event {
|
|||||||
return EventMapper.map(this)
|
return EventMapper.map(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun Event.toEntity(roomId: String): EventEntity {
|
internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long? = null): EventEntity {
|
||||||
return EventMapper.map(this, roomId)
|
return EventMapper.map(this, roomId).apply {
|
||||||
|
this.sendState = sendState
|
||||||
|
this.ageLocalTs = ageLocalTs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,13 +37,17 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
|
|||||||
return TimelineEvent(
|
return TimelineEvent(
|
||||||
root = timelineEventEntity.root?.asDomain()
|
root = timelineEventEntity.root?.asDomain()
|
||||||
?: Event("", timelineEventEntity.eventId),
|
?: Event("", timelineEventEntity.eventId),
|
||||||
|
eventId = timelineEventEntity.eventId,
|
||||||
annotations = timelineEventEntity.annotations?.asDomain(),
|
annotations = timelineEventEntity.annotations?.asDomain(),
|
||||||
localId = timelineEventEntity.localId,
|
localId = timelineEventEntity.localId,
|
||||||
displayIndex = timelineEventEntity.root?.displayIndex ?: 0,
|
displayIndex = timelineEventEntity.displayIndex,
|
||||||
senderName = timelineEventEntity.senderName,
|
senderName = timelineEventEntity.senderName,
|
||||||
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
|
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
|
||||||
senderAvatar = timelineEventEntity.senderAvatar,
|
senderAvatar = timelineEventEntity.senderAvatar,
|
||||||
readReceipts = readReceipts?.sortedByDescending {
|
readReceipts = readReceipts
|
||||||
|
?.distinctBy {
|
||||||
|
it.user
|
||||||
|
}?.sortedByDescending {
|
||||||
it.originServerTs
|
it.originServerTs
|
||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
)
|
)
|
||||||
|
@ -24,14 +24,10 @@ import io.realm.annotations.LinkingObjects
|
|||||||
|
|
||||||
internal open class ChunkEntity(@Index var prevToken: String? = null,
|
internal open class ChunkEntity(@Index var prevToken: String? = null,
|
||||||
@Index var nextToken: String? = null,
|
@Index var nextToken: String? = null,
|
||||||
|
var stateEvents: RealmList<EventEntity> = RealmList(),
|
||||||
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
||||||
@Index var isLastForward: Boolean = false,
|
@Index var isLastForward: Boolean = false,
|
||||||
@Index var isLastBackward: Boolean = false,
|
@Index var isLastBackward: Boolean = false
|
||||||
var backwardsDisplayIndex: Int? = null,
|
|
||||||
var forwardsDisplayIndex: Int? = null,
|
|
||||||
var backwardsStateIndex: Int? = null,
|
|
||||||
var forwardsStateIndex: Int? = null,
|
|
||||||
var isUnlinked: Boolean = false
|
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
fun identifier() = "${prevToken}_$nextToken"
|
fun identifier() = "${prevToken}_$nextToken"
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.database.model
|
||||||
|
|
||||||
|
import io.realm.RealmObject
|
||||||
|
import io.realm.annotations.Index
|
||||||
|
|
||||||
|
internal open class CurrentStateEventEntity(var eventId: String = "",
|
||||||
|
var root: EventEntity? = null,
|
||||||
|
@Index var roomId: String = "",
|
||||||
|
@Index var type: String = "",
|
||||||
|
@Index var stateKey: String = ""
|
||||||
|
) : RealmObject() {
|
||||||
|
companion object
|
||||||
|
}
|
@ -21,11 +21,10 @@ import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
|||||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
import io.realm.RealmObject
|
import io.realm.RealmObject
|
||||||
import io.realm.RealmResults
|
|
||||||
import io.realm.annotations.Index
|
import io.realm.annotations.Index
|
||||||
import io.realm.annotations.LinkingObjects
|
import io.realm.annotations.PrimaryKey
|
||||||
|
|
||||||
internal open class EventEntity(@Index var eventId: String = "",
|
internal open class EventEntity(@PrimaryKey var eventId: String = "",
|
||||||
@Index var roomId: String = "",
|
@Index var roomId: String = "",
|
||||||
@Index var type: String = "",
|
@Index var type: String = "",
|
||||||
var content: String? = null,
|
var content: String? = null,
|
||||||
@ -36,20 +35,11 @@ internal open class EventEntity(@Index var eventId: String = "",
|
|||||||
var age: Long? = 0,
|
var age: Long? = 0,
|
||||||
var unsignedData: String? = null,
|
var unsignedData: String? = null,
|
||||||
var redacts: String? = null,
|
var redacts: String? = null,
|
||||||
@Index var stateIndex: Int = 0,
|
|
||||||
@Index var displayIndex: Int = 0,
|
|
||||||
@Index var isUnlinked: Boolean = false,
|
|
||||||
var decryptionResultJson: String? = null,
|
var decryptionResultJson: String? = null,
|
||||||
var decryptionErrorCode: String? = null,
|
var decryptionErrorCode: String? = null,
|
||||||
var ageLocalTs: Long? = null
|
var ageLocalTs: Long? = null
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
enum class LinkFilterMode {
|
|
||||||
LINKED_ONLY,
|
|
||||||
UNLINKED_ONLY,
|
|
||||||
BOTH
|
|
||||||
}
|
|
||||||
|
|
||||||
private var sendStateStr: String = SendState.UNKNOWN.name
|
private var sendStateStr: String = SendState.UNKNOWN.name
|
||||||
|
|
||||||
var sendState: SendState
|
var sendState: SendState
|
||||||
@ -62,12 +52,6 @@ internal open class EventEntity(@Index var eventId: String = "",
|
|||||||
|
|
||||||
companion object
|
companion object
|
||||||
|
|
||||||
@LinkingObjects("untimelinedStateEvents")
|
|
||||||
val room: RealmResults<RoomEntity>? = null
|
|
||||||
|
|
||||||
@LinkingObjects("root")
|
|
||||||
val timelineEventEntity: RealmResults<TimelineEventEntity>? = null
|
|
||||||
|
|
||||||
fun setDecryptionResult(result: MXEventDecryptionResult) {
|
fun setDecryptionResult(result: MXEventDecryptionResult) {
|
||||||
val decryptionResult = OlmDecryptionResult(
|
val decryptionResult = OlmDecryptionResult(
|
||||||
payload = result.clearEvent,
|
payload = result.clearEvent,
|
||||||
@ -78,6 +62,5 @@ internal open class EventEntity(@Index var eventId: String = "",
|
|||||||
val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java)
|
val adapter = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java)
|
||||||
decryptionResultJson = adapter.toJson(decryptionResult)
|
decryptionResultJson = adapter.toJson(decryptionResult)
|
||||||
decryptionErrorCode = null
|
decryptionErrorCode = null
|
||||||
timelineEventEntity?.firstOrNull()?.root = this
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@ import io.realm.annotations.PrimaryKey
|
|||||||
|
|
||||||
internal open class RoomEntity(@PrimaryKey var roomId: String = "",
|
internal open class RoomEntity(@PrimaryKey var roomId: String = "",
|
||||||
var chunks: RealmList<ChunkEntity> = RealmList(),
|
var chunks: RealmList<ChunkEntity> = RealmList(),
|
||||||
var untimelinedStateEvents: RealmList<EventEntity> = RealmList(),
|
|
||||||
var sendingTimelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
var sendingTimelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
||||||
var areAllMembersLoaded: Boolean = false
|
var areAllMembersLoaded: Boolean = false
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
@ -38,9 +38,9 @@ import io.realm.annotations.RealmModule
|
|||||||
IgnoredUserEntity::class,
|
IgnoredUserEntity::class,
|
||||||
BreadcrumbsEntity::class,
|
BreadcrumbsEntity::class,
|
||||||
EventAnnotationsSummaryEntity::class,
|
EventAnnotationsSummaryEntity::class,
|
||||||
ReferencesAggregatedSummaryEntity::class,
|
|
||||||
ReactionAggregatedSummaryEntity::class,
|
ReactionAggregatedSummaryEntity::class,
|
||||||
EditAggregatedSummaryEntity::class,
|
EditAggregatedSummaryEntity::class,
|
||||||
|
ReferencesAggregatedSummaryEntity::class,
|
||||||
PushRulesEntity::class,
|
PushRulesEntity::class,
|
||||||
PushRuleEntity::class,
|
PushRuleEntity::class,
|
||||||
PushConditionEntity::class,
|
PushConditionEntity::class,
|
||||||
@ -51,6 +51,7 @@ import io.realm.annotations.RealmModule
|
|||||||
UserDraftsEntity::class,
|
UserDraftsEntity::class,
|
||||||
DraftEntity::class,
|
DraftEntity::class,
|
||||||
HomeServerCapabilitiesEntity::class,
|
HomeServerCapabilitiesEntity::class,
|
||||||
RoomMemberSummaryEntity::class
|
RoomMemberSummaryEntity::class,
|
||||||
|
CurrentStateEventEntity::class
|
||||||
])
|
])
|
||||||
internal class SessionRealmModule
|
internal class SessionRealmModule
|
||||||
|
@ -24,6 +24,7 @@ import io.realm.annotations.LinkingObjects
|
|||||||
internal open class TimelineEventEntity(var localId: Long = 0,
|
internal open class TimelineEventEntity(var localId: Long = 0,
|
||||||
@Index var eventId: String = "",
|
@Index var eventId: String = "",
|
||||||
@Index var roomId: String = "",
|
@Index var roomId: String = "",
|
||||||
|
@Index var displayIndex: Int = 0,
|
||||||
var root: EventEntity? = null,
|
var root: EventEntity? = null,
|
||||||
var annotations: EventAnnotationsSummaryEntity? = null,
|
var annotations: EventAnnotationsSummaryEntity? = null,
|
||||||
var senderName: String? = null,
|
var senderName: String? = null,
|
||||||
|
@ -60,12 +60,10 @@ internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: Str
|
|||||||
internal fun ChunkEntity.Companion.create(
|
internal fun ChunkEntity.Companion.create(
|
||||||
realm: Realm,
|
realm: Realm,
|
||||||
prevToken: String?,
|
prevToken: String?,
|
||||||
nextToken: String?,
|
nextToken: String?
|
||||||
isUnlinked: Boolean
|
|
||||||
): ChunkEntity {
|
): ChunkEntity {
|
||||||
return realm.createObject<ChunkEntity>().apply {
|
return realm.createObject<ChunkEntity>().apply {
|
||||||
this.prevToken = prevToken
|
this.prevToken = prevToken
|
||||||
this.nextToken = nextToken
|
this.nextToken = nextToken
|
||||||
this.isUnlinked = isUnlinked
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.database.query
|
||||||
|
|
||||||
|
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntityFields
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmQuery
|
||||||
|
import io.realm.kotlin.createObject
|
||||||
|
|
||||||
|
internal fun CurrentStateEventEntity.Companion.where(realm: Realm, roomId: String, type: String): RealmQuery<CurrentStateEventEntity> {
|
||||||
|
return realm.where(CurrentStateEventEntity::class.java)
|
||||||
|
.equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId)
|
||||||
|
.equalTo(CurrentStateEventEntityFields.TYPE, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun CurrentStateEventEntity.Companion.whereStateKey(realm: Realm, roomId: String, type: String, stateKey: String)
|
||||||
|
: RealmQuery<CurrentStateEventEntity> {
|
||||||
|
return where(realm = realm, roomId = roomId, type = type)
|
||||||
|
.equalTo(CurrentStateEventEntityFields.STATE_KEY, stateKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun CurrentStateEventEntity.Companion.getOrNull(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity? {
|
||||||
|
return whereStateKey(realm = realm, roomId = roomId, type = type, stateKey = stateKey).findFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun CurrentStateEventEntity.Companion.getOrCreate(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity {
|
||||||
|
return getOrNull(realm = realm, roomId = roomId, stateKey = stateKey, type = type) ?: create(realm, roomId, stateKey, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun create(realm: Realm, roomId: String, stateKey: String, type: String): CurrentStateEventEntity {
|
||||||
|
return realm.createObject<CurrentStateEventEntity>().apply {
|
||||||
|
this.type = type
|
||||||
|
this.roomId = roomId
|
||||||
|
this.stateKey = stateKey
|
||||||
|
}
|
||||||
|
}
|
@ -17,12 +17,10 @@
|
|||||||
package im.vector.matrix.android.internal.database.query
|
package im.vector.matrix.android.internal.database.query
|
||||||
|
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.*
|
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmList
|
import io.realm.RealmList
|
||||||
import io.realm.RealmQuery
|
import io.realm.RealmQuery
|
||||||
import io.realm.Sort
|
|
||||||
import io.realm.kotlin.where
|
import io.realm.kotlin.where
|
||||||
|
|
||||||
internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<EventEntity> {
|
internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<EventEntity> {
|
||||||
@ -35,61 +33,28 @@ internal fun EventEntity.Companion.where(realm: Realm, eventIds: List<String>):
|
|||||||
.`in`(EventEntityFields.EVENT_ID, eventIds.toTypedArray())
|
.`in`(EventEntityFields.EVENT_ID, eventIds.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun EventEntity.Companion.where(realm: Realm,
|
internal fun EventEntity.Companion.whereType(realm: Realm,
|
||||||
roomId: String? = null,
|
type: String,
|
||||||
type: String? = null,
|
roomId: String? = null
|
||||||
linkFilterMode: EventEntity.LinkFilterMode = LINKED_ONLY): RealmQuery<EventEntity> {
|
): RealmQuery<EventEntity> {
|
||||||
val query = realm.where<EventEntity>()
|
val query = realm.where<EventEntity>()
|
||||||
if (roomId != null) {
|
if (roomId != null) {
|
||||||
query.equalTo(EventEntityFields.ROOM_ID, roomId)
|
query.equalTo(EventEntityFields.ROOM_ID, roomId)
|
||||||
}
|
}
|
||||||
if (type != null) {
|
return query.equalTo(EventEntityFields.TYPE, type)
|
||||||
query.equalTo(EventEntityFields.TYPE, type)
|
|
||||||
}
|
|
||||||
return when (linkFilterMode) {
|
|
||||||
LINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, false)
|
|
||||||
UNLINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, true)
|
|
||||||
BOTH -> query
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun EventEntity.Companion.types(realm: Realm,
|
internal fun EventEntity.Companion.whereTypes(realm: Realm,
|
||||||
typeList: List<String> = emptyList()): RealmQuery<EventEntity> {
|
typeList: List<String> = emptyList(),
|
||||||
|
roomId: String? = null): RealmQuery<EventEntity> {
|
||||||
val query = realm.where<EventEntity>()
|
val query = realm.where<EventEntity>()
|
||||||
query.`in`(EventEntityFields.TYPE, typeList.toTypedArray())
|
query.`in`(EventEntityFields.TYPE, typeList.toTypedArray())
|
||||||
|
if (roomId != null) {
|
||||||
|
query.equalTo(EventEntityFields.ROOM_ID, roomId)
|
||||||
|
}
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun RealmQuery<EventEntity>.descending(since: Int? = null, strict: Boolean = false): RealmQuery<EventEntity> {
|
|
||||||
if (since != null) {
|
|
||||||
if (strict) {
|
|
||||||
this.lessThan(EventEntityFields.STATE_INDEX, since)
|
|
||||||
} else {
|
|
||||||
this.lessThanOrEqualTo(EventEntityFields.STATE_INDEX, since)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun RealmQuery<EventEntity>.ascending(from: Int? = null, strict: Boolean = true): RealmQuery<EventEntity> {
|
|
||||||
if (from != null) {
|
|
||||||
if (strict) {
|
|
||||||
this.greaterThan(EventEntityFields.STATE_INDEX, from)
|
|
||||||
} else {
|
|
||||||
this.greaterThanOrEqualTo(EventEntityFields.STATE_INDEX, from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun RealmQuery<EventEntity>.next(from: Int? = null, strict: Boolean = true): EventEntity? {
|
|
||||||
return this.ascending(from, strict).findFirst()
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun RealmQuery<EventEntity>.prev(since: Int? = null, strict: Boolean = false): EventEntity? {
|
|
||||||
return descending(since, strict).findFirst()
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun RealmList<EventEntity>.find(eventId: String): EventEntity? {
|
internal fun RealmList<EventEntity>.find(eventId: String): EventEntity? {
|
||||||
return this.where()
|
return this.where()
|
||||||
.equalTo(EventEntityFields.EVENT_ID, eventId)
|
.equalTo(EventEntityFields.EVENT_ID, eventId)
|
||||||
|
@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.events.model.LocalEcho
|
|||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||||
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
|
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
|
||||||
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
|
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
|
|
||||||
internal fun isEventRead(monarchy: Monarchy,
|
internal fun isEventRead(monarchy: Monarchy,
|
||||||
@ -36,16 +37,15 @@ internal fun isEventRead(monarchy: Monarchy,
|
|||||||
|
|
||||||
monarchy.doWithRealm { realm ->
|
monarchy.doWithRealm { realm ->
|
||||||
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return@doWithRealm
|
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return@doWithRealm
|
||||||
val eventToCheck = liveChunk.timelineEvents.find(eventId)?.root
|
val eventToCheck = liveChunk.timelineEvents.find(eventId)
|
||||||
|
isEventRead = if (eventToCheck == null || eventToCheck.root?.sender == userId) {
|
||||||
isEventRead = if (eventToCheck?.sender == userId) {
|
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst()
|
val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst()
|
||||||
?: return@doWithRealm
|
?: return@doWithRealm
|
||||||
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
|
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.displayIndex
|
||||||
?: Int.MIN_VALUE
|
?: Int.MIN_VALUE
|
||||||
val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE
|
val eventToCheckIndex = eventToCheck.displayIndex
|
||||||
|
|
||||||
eventToCheckIndex <= readReceiptIndex
|
eventToCheckIndex <= readReceiptIndex
|
||||||
}
|
}
|
||||||
@ -61,13 +61,17 @@ internal fun isReadMarkerMoreRecent(monarchy: Monarchy,
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||||
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return false
|
val eventToCheck = TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()
|
||||||
val eventToCheck = liveChunk.timelineEvents.find(eventId)?.root
|
val eventToCheckChunk = eventToCheck?.chunk?.firstOrNull()
|
||||||
|
|
||||||
val readMarker = ReadMarkerEntity.where(realm, roomId).findFirst() ?: return false
|
val readMarker = ReadMarkerEntity.where(realm, roomId).findFirst() ?: return false
|
||||||
val readMarkerIndex = liveChunk.timelineEvents.find(readMarker.eventId)?.root?.displayIndex
|
val readMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = readMarker.eventId).findFirst()
|
||||||
?: Int.MIN_VALUE
|
val readMarkerChunk = readMarkerEvent?.chunk?.firstOrNull()
|
||||||
|
if (eventToCheckChunk == readMarkerChunk) {
|
||||||
|
val readMarkerIndex = readMarkerEvent?.displayIndex ?: Int.MIN_VALUE
|
||||||
val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE
|
val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE
|
||||||
eventToCheckIndex <= readMarkerIndex
|
eventToCheckIndex <= readMarkerIndex
|
||||||
|
} else {
|
||||||
|
eventToCheckChunk?.isLastForward == false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.database.query
|
|||||||
|
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import im.vector.matrix.android.internal.database.model.*
|
import im.vector.matrix.android.internal.database.model.*
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.*
|
|
||||||
import io.realm.*
|
import io.realm.*
|
||||||
import io.realm.kotlin.where
|
import io.realm.kotlin.where
|
||||||
|
|
||||||
@ -34,22 +33,10 @@ internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, e
|
|||||||
.`in`(TimelineEventEntityFields.EVENT_ID, eventIds.toTypedArray())
|
.`in`(TimelineEventEntityFields.EVENT_ID, eventIds.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun TimelineEventEntity.Companion.where(realm: Realm,
|
internal fun TimelineEventEntity.Companion.whereRoomId(realm: Realm,
|
||||||
roomId: String? = null,
|
roomId: String): RealmQuery<TimelineEventEntity> {
|
||||||
type: String? = null,
|
return realm.where<TimelineEventEntity>()
|
||||||
linkFilterMode: EventEntity.LinkFilterMode = LINKED_ONLY): RealmQuery<TimelineEventEntity> {
|
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
|
||||||
val query = realm.where<TimelineEventEntity>()
|
|
||||||
if (roomId != null) {
|
|
||||||
query.equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
|
|
||||||
}
|
|
||||||
if (type != null) {
|
|
||||||
query.equalTo(TimelineEventEntityFields.ROOT.TYPE, type)
|
|
||||||
}
|
|
||||||
return when (linkFilterMode) {
|
|
||||||
LINKED_ONLY -> query.equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, false)
|
|
||||||
UNLINKED_ONLY -> query.equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, true)
|
|
||||||
BOTH -> query
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List<TimelineEventEntity> {
|
internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List<TimelineEventEntity> {
|
||||||
@ -71,7 +58,7 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
|
|||||||
liveEvents
|
liveEvents
|
||||||
}
|
}
|
||||||
return query
|
return query
|
||||||
?.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
|
?.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
?.findFirst()
|
?.findFirst()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,32 +70,6 @@ internal fun RealmQuery<TimelineEventEntity>.filterTypes(filterTypes: List<Strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun RealmQuery<TimelineEventEntity>.next(from: Int? = null, strict: Boolean = true): TimelineEventEntity? {
|
|
||||||
if (from != null) {
|
|
||||||
if (strict) {
|
|
||||||
this.greaterThan(TimelineEventEntityFields.ROOT.STATE_INDEX, from)
|
|
||||||
} else {
|
|
||||||
this.greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.STATE_INDEX, from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
.sort(TimelineEventEntityFields.ROOT.STATE_INDEX, Sort.ASCENDING)
|
|
||||||
.findFirst()
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun RealmQuery<TimelineEventEntity>.prev(since: Int? = null, strict: Boolean = false): TimelineEventEntity? {
|
|
||||||
if (since != null) {
|
|
||||||
if (strict) {
|
|
||||||
this.lessThan(TimelineEventEntityFields.ROOT.STATE_INDEX, since)
|
|
||||||
} else {
|
|
||||||
this.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.STATE_INDEX, since)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
.sort(TimelineEventEntityFields.ROOT.STATE_INDEX, Sort.DESCENDING)
|
|
||||||
.findFirst()
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun RealmList<TimelineEventEntity>.find(eventId: String): TimelineEventEntity? {
|
internal fun RealmList<TimelineEventEntity>.find(eventId: String): TimelineEventEntity? {
|
||||||
return this.where()
|
return this.where()
|
||||||
.equalTo(TimelineEventEntityFields.EVENT_ID, eventId)
|
.equalTo(TimelineEventEntityFields.EVENT_ID, eventId)
|
||||||
|
@ -27,7 +27,6 @@ import im.vector.matrix.android.api.auth.AuthenticationService
|
|||||||
import im.vector.matrix.android.internal.SessionManager
|
import im.vector.matrix.android.internal.SessionManager
|
||||||
import im.vector.matrix.android.internal.auth.AuthModule
|
import im.vector.matrix.android.internal.auth.AuthModule
|
||||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||||
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
|
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
|
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
|
||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
@ -59,8 +58,6 @@ internal interface MatrixComponent {
|
|||||||
|
|
||||||
fun sessionParamsStore(): SessionParamsStore
|
fun sessionParamsStore(): SessionParamsStore
|
||||||
|
|
||||||
fun networkConnectivityChecker(): NetworkConnectivityChecker
|
|
||||||
|
|
||||||
fun backgroundDetectionObserver(): BackgroundDetectionObserver
|
fun backgroundDetectionObserver(): BackgroundDetectionObserver
|
||||||
|
|
||||||
fun sessionManager(): SessionManager
|
fun sessionManager(): SessionManager
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.network
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.Network
|
||||||
|
import android.os.Build
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface NetworkCallbackStrategy {
|
||||||
|
fun register(hasChanged: () -> Unit)
|
||||||
|
fun unregister()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class FallbackNetworkCallbackStrategy @Inject constructor(private val context: Context,
|
||||||
|
private val networkInfoReceiver: NetworkInfoReceiver) : NetworkCallbackStrategy {
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
|
||||||
|
|
||||||
|
override fun register(hasChanged: () -> Unit) {
|
||||||
|
networkInfoReceiver.isConnectedCallback = {
|
||||||
|
hasChanged()
|
||||||
|
}
|
||||||
|
context.registerReceiver(networkInfoReceiver, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unregister() {
|
||||||
|
networkInfoReceiver.isConnectedCallback = null
|
||||||
|
context.unregisterReceiver(networkInfoReceiver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N)
|
||||||
|
internal class PreferredNetworkCallbackStrategy @Inject constructor(context: Context) : NetworkCallbackStrategy {
|
||||||
|
|
||||||
|
private var hasChangedCallback: (() -> Unit)? = null
|
||||||
|
private val conn = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
hasChangedCallback?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
hasChangedCallback?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun register(hasChanged: () -> Unit) {
|
||||||
|
hasChangedCallback = hasChanged
|
||||||
|
conn.registerDefaultNetworkCallback(networkCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unregister() {
|
||||||
|
hasChangedCallback = null
|
||||||
|
conn.unregisterNetworkCallback(networkCallback)
|
||||||
|
}
|
||||||
|
}
|
@ -16,112 +16,94 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.network
|
package im.vector.matrix.android.internal.network
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import com.novoda.merlin.Merlin
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
import com.novoda.merlin.MerlinsBeard
|
import im.vector.matrix.android.internal.session.homeserver.HomeServerPinger
|
||||||
import im.vector.matrix.android.internal.di.MatrixScope
|
|
||||||
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
|
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
|
||||||
import timber.log.Timber
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.suspendCoroutine
|
|
||||||
|
|
||||||
@MatrixScope
|
interface NetworkConnectivityChecker {
|
||||||
internal class NetworkConnectivityChecker @Inject constructor(private val context: Context,
|
/**
|
||||||
private val backgroundDetectionObserver: BackgroundDetectionObserver)
|
* Returns true when internet is available
|
||||||
: BackgroundDetectionObserver.Listener {
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
fun hasInternetAccess(forcePing: Boolean): Boolean
|
||||||
|
|
||||||
private val merlin = Merlin.Builder()
|
fun register(listener: Listener)
|
||||||
.withConnectableCallbacks()
|
fun unregister(listener: Listener)
|
||||||
.withDisconnectableCallbacks()
|
|
||||||
.build(context)
|
|
||||||
|
|
||||||
private val merlinsBeard = MerlinsBeard.Builder().build(context)
|
interface Listener {
|
||||||
|
fun onConnectivityChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val listeners = Collections.synchronizedSet(LinkedHashSet<Listener>())
|
@SessionScope
|
||||||
private var hasInternetAccess = merlinsBeard.isConnected
|
internal class DefaultNetworkConnectivityChecker @Inject constructor(private val homeServerPinger: HomeServerPinger,
|
||||||
|
private val backgroundDetectionObserver: BackgroundDetectionObserver,
|
||||||
|
private val networkCallbackStrategy: NetworkCallbackStrategy)
|
||||||
|
: NetworkConnectivityChecker {
|
||||||
|
|
||||||
init {
|
private val hasInternetAccess = AtomicBoolean(true)
|
||||||
backgroundDetectionObserver.register(this)
|
private val listeners = Collections.synchronizedSet(LinkedHashSet<NetworkConnectivityChecker.Listener>())
|
||||||
|
private val backgroundDetectionObserverListener = object : BackgroundDetectionObserver.Listener {
|
||||||
|
override fun onMoveToForeground() {
|
||||||
|
bind()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMoveToBackground() {
|
||||||
|
unbind()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true when internet is available
|
* Returns true when internet is available
|
||||||
*/
|
*/
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun hasInternetAccess(): Boolean {
|
override fun hasInternetAccess(forcePing: Boolean): Boolean {
|
||||||
// If we are in background we have unbound merlin, so we have to check
|
return if (forcePing) {
|
||||||
return if (backgroundDetectionObserver.isInBackground) {
|
runBlocking {
|
||||||
merlinsBeard.hasInternetAccess()
|
homeServerPinger.canReachHomeServer()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
hasInternetAccess
|
hasInternetAccess.get()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMoveToForeground() {
|
override fun register(listener: NetworkConnectivityChecker.Listener) {
|
||||||
merlin.bind()
|
if (listeners.isEmpty()) {
|
||||||
merlinsBeard.hasInternetAccess {
|
if (backgroundDetectionObserver.isInBackground) {
|
||||||
hasInternetAccess = it
|
unbind()
|
||||||
}
|
|
||||||
merlin.registerDisconnectable {
|
|
||||||
if (hasInternetAccess) {
|
|
||||||
Timber.v("On Disconnect")
|
|
||||||
hasInternetAccess = false
|
|
||||||
val localListeners = listeners.toList()
|
|
||||||
localListeners.forEach {
|
|
||||||
it.onDisconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
merlin.registerConnectable {
|
|
||||||
if (!hasInternetAccess) {
|
|
||||||
Timber.v("On Connect")
|
|
||||||
hasInternetAccess = true
|
|
||||||
val localListeners = listeners.toList()
|
|
||||||
localListeners.forEach {
|
|
||||||
it.onConnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMoveToBackground() {
|
|
||||||
merlin.unbind()
|
|
||||||
}
|
|
||||||
|
|
||||||
// In background you won't get notification as merlin is unbound
|
|
||||||
suspend fun waitUntilConnected() {
|
|
||||||
if (hasInternetAccess) {
|
|
||||||
return
|
|
||||||
} else {
|
} else {
|
||||||
Timber.v("Waiting for network...")
|
bind()
|
||||||
suspendCoroutine<Unit> { continuation ->
|
|
||||||
register(object : Listener {
|
|
||||||
override fun onConnect() {
|
|
||||||
unregister(this)
|
|
||||||
Timber.v("Connected to network...")
|
|
||||||
continuation.resume(Unit)
|
|
||||||
}
|
}
|
||||||
})
|
backgroundDetectionObserver.register(backgroundDetectionObserverListener)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun register(listener: Listener) {
|
|
||||||
listeners.add(listener)
|
listeners.add(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregister(listener: Listener) {
|
override fun unregister(listener: NetworkConnectivityChecker.Listener) {
|
||||||
listeners.remove(listener)
|
listeners.remove(listener)
|
||||||
|
if (listeners.isEmpty()) {
|
||||||
|
backgroundDetectionObserver.unregister(backgroundDetectionObserverListener)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Listener {
|
private fun bind() {
|
||||||
fun onConnect() {
|
networkCallbackStrategy.register {
|
||||||
|
val localListeners = listeners.toList()
|
||||||
|
localListeners.forEach {
|
||||||
|
it.onConnectivityChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
homeServerPinger.canReachHomeServer {
|
||||||
|
hasInternetAccess.set(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDisconnect() {
|
private fun unbind() {
|
||||||
}
|
networkCallbackStrategy.unregister()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.network
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkInfo
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class NetworkInfoReceiver @Inject constructor() : BroadcastReceiver() {
|
||||||
|
|
||||||
|
var isConnectedCallback: ((Boolean) -> Unit)? = null
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val conn = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
val networkInfo: NetworkInfo? = conn.activeNetworkInfo
|
||||||
|
isConnectedCallback?.invoke(networkInfo?.isConnected ?: false)
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.network
|
|||||||
|
|
||||||
import im.vector.matrix.android.api.failure.Failure
|
import im.vector.matrix.android.api.failure.Failure
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@ -27,11 +28,17 @@ internal suspend inline fun <DATA> executeRequest(eventBus: EventBus?,
|
|||||||
|
|
||||||
internal class Request<DATA>(private val eventBus: EventBus?) {
|
internal class Request<DATA>(private val eventBus: EventBus?) {
|
||||||
|
|
||||||
|
var isRetryable = false
|
||||||
|
var initialDelay: Long = 100L
|
||||||
|
var maxDelay: Long = 10_000L
|
||||||
|
var maxRetryCount = Int.MAX_VALUE
|
||||||
|
private var currentRetryCount = 0
|
||||||
|
private var currentDelay = initialDelay
|
||||||
lateinit var apiCall: Call<DATA>
|
lateinit var apiCall: Call<DATA>
|
||||||
|
|
||||||
suspend fun execute(): DATA {
|
suspend fun execute(): DATA {
|
||||||
return try {
|
return try {
|
||||||
val response = apiCall.awaitResponse()
|
val response = apiCall.clone().awaitResponse()
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
response.body()
|
response.body()
|
||||||
?: throw IllegalStateException("The request returned a null body")
|
?: throw IllegalStateException("The request returned a null body")
|
||||||
@ -39,6 +46,11 @@ internal class Request<DATA>(private val eventBus: EventBus?) {
|
|||||||
throw response.toFailure(eventBus)
|
throw response.toFailure(eventBus)
|
||||||
}
|
}
|
||||||
} catch (exception: Throwable) {
|
} catch (exception: Throwable) {
|
||||||
|
if (isRetryable && currentRetryCount++ < maxRetryCount && exception is IOException) {
|
||||||
|
delay(currentDelay)
|
||||||
|
currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay)
|
||||||
|
return execute()
|
||||||
|
} else {
|
||||||
throw when (exception) {
|
throw when (exception) {
|
||||||
is IOException -> Failure.NetworkConnection(exception)
|
is IOException -> Failure.NetworkConnection(exception)
|
||||||
is Failure.ServerError,
|
is Failure.ServerError,
|
||||||
@ -48,4 +60,5 @@ internal class Request<DATA>(private val eventBus: EventBus?) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package im.vector.matrix.android.internal.session
|
package im.vector.matrix.android.internal.session
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
@ -46,6 +47,11 @@ import im.vector.matrix.android.internal.di.Unauthenticated
|
|||||||
import im.vector.matrix.android.internal.di.UserId
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
import im.vector.matrix.android.internal.di.UserMd5
|
import im.vector.matrix.android.internal.di.UserMd5
|
||||||
import im.vector.matrix.android.internal.network.AccessTokenInterceptor
|
import im.vector.matrix.android.internal.network.AccessTokenInterceptor
|
||||||
|
import im.vector.matrix.android.internal.network.DefaultNetworkConnectivityChecker
|
||||||
|
import im.vector.matrix.android.internal.network.FallbackNetworkCallbackStrategy
|
||||||
|
import im.vector.matrix.android.internal.network.NetworkCallbackStrategy
|
||||||
|
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
|
||||||
|
import im.vector.matrix.android.internal.network.PreferredNetworkCallbackStrategy
|
||||||
import im.vector.matrix.android.internal.network.RetrofitFactory
|
import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||||
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
|
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
|
||||||
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
||||||
@ -61,6 +67,7 @@ import okhttp3.OkHttpClient
|
|||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import javax.inject.Provider
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
internal abstract class SessionModule {
|
internal abstract class SessionModule {
|
||||||
@ -69,6 +76,11 @@ internal abstract class SessionModule {
|
|||||||
companion object {
|
companion object {
|
||||||
internal fun getKeyAlias(userMd5: String) = "session_db_$userMd5"
|
internal fun getKeyAlias(userMd5: String) = "session_db_$userMd5"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rules:
|
||||||
|
* Annotate methods with @SessionScope only the @Provides annotated methods with computation and logic.
|
||||||
|
*/
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Provides
|
@Provides
|
||||||
fun providesHomeServerConnectionConfig(sessionParams: SessionParams): HomeServerConnectionConfig {
|
fun providesHomeServerConnectionConfig(sessionParams: SessionParams): HomeServerConnectionConfig {
|
||||||
@ -84,6 +96,7 @@ internal abstract class SessionModule {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
@UserId
|
@UserId
|
||||||
@Provides
|
@Provides
|
||||||
|
@SessionScope
|
||||||
fun providesUserId(credentials: Credentials): String {
|
fun providesUserId(credentials: Credentials): String {
|
||||||
return credentials.userId
|
return credentials.userId
|
||||||
}
|
}
|
||||||
@ -98,6 +111,7 @@ internal abstract class SessionModule {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
@UserMd5
|
@UserMd5
|
||||||
@Provides
|
@Provides
|
||||||
|
@SessionScope
|
||||||
fun providesUserMd5(@UserId userId: String): String {
|
fun providesUserMd5(@UserId userId: String): String {
|
||||||
return userId.md5()
|
return userId.md5()
|
||||||
}
|
}
|
||||||
@ -105,6 +119,7 @@ internal abstract class SessionModule {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
@SessionId
|
@SessionId
|
||||||
@Provides
|
@Provides
|
||||||
|
@SessionScope
|
||||||
fun providesSessionId(credentials: Credentials): String {
|
fun providesSessionId(credentials: Credentials): String {
|
||||||
return credentials.sessionId()
|
return credentials.sessionId()
|
||||||
}
|
}
|
||||||
@ -189,6 +204,19 @@ internal abstract class SessionModule {
|
|||||||
return EventBus.builder().build()
|
return EventBus.builder().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@SessionScope
|
||||||
|
fun providesNetworkCallbackStrategy(fallbackNetworkCallbackStrategy: Provider<FallbackNetworkCallbackStrategy>,
|
||||||
|
preferredNetworkCallbackStrategy: Provider<PreferredNetworkCallbackStrategy>
|
||||||
|
): NetworkCallbackStrategy {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
preferredNetworkCallbackStrategy.get()
|
||||||
|
} else {
|
||||||
|
fallbackNetworkCallbackStrategy.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Provides
|
@Provides
|
||||||
@SessionScope
|
@SessionScope
|
||||||
@ -200,6 +228,9 @@ internal abstract class SessionModule {
|
|||||||
@Binds
|
@Binds
|
||||||
abstract fun bindSession(session: DefaultSession): Session
|
abstract fun bindSession(session: DefaultSession): Session
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindNetworkConnectivityChecker(networkConnectivityChecker: DefaultNetworkConnectivityChecker): NetworkConnectivityChecker
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindGroupSummaryUpdater(groupSummaryUpdater: GroupSummaryUpdater): LiveEntityObserver
|
abstract fun bindGroupSummaryUpdater(groupSummaryUpdater: GroupSummaryUpdater): LiveEntityObserver
|
||||||
|
@ -25,6 +25,7 @@ import im.vector.matrix.android.internal.session.group.model.GroupRooms
|
|||||||
import im.vector.matrix.android.internal.session.group.model.GroupSummaryResponse
|
import im.vector.matrix.android.internal.session.group.model.GroupSummaryResponse
|
||||||
import im.vector.matrix.android.internal.session.group.model.GroupUsers
|
import im.vector.matrix.android.internal.session.group.model.GroupUsers
|
||||||
import im.vector.matrix.android.internal.task.Task
|
import im.vector.matrix.android.internal.task.Task
|
||||||
|
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -53,12 +54,12 @@ internal class DefaultGetGroupDataTask @Inject constructor(
|
|||||||
insertInDb(groupSummary, groupRooms, groupUsers, groupId)
|
insertInDb(groupSummary, groupRooms, groupUsers, groupId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun insertInDb(groupSummary: GroupSummaryResponse,
|
private suspend fun insertInDb(groupSummary: GroupSummaryResponse,
|
||||||
groupRooms: GroupRooms,
|
groupRooms: GroupRooms,
|
||||||
groupUsers: GroupUsers,
|
groupUsers: GroupUsers,
|
||||||
groupId: String) {
|
groupId: String) {
|
||||||
monarchy
|
monarchy
|
||||||
.writeAsync { realm ->
|
.awaitTransaction { realm ->
|
||||||
val groupSummaryEntity = GroupSummaryEntity.where(realm, groupId).findFirst()
|
val groupSummaryEntity = GroupSummaryEntity.where(realm, groupId).findFirst()
|
||||||
?: realm.createObject(GroupSummaryEntity::class.java, groupId)
|
?: realm.createObject(GroupSummaryEntity::class.java, groupId)
|
||||||
|
|
||||||
|
@ -27,4 +27,10 @@ internal interface CapabilitiesAPI {
|
|||||||
*/
|
*/
|
||||||
@GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config")
|
@GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config")
|
||||||
fun getUploadCapabilities(): Call<GetUploadCapabilitiesResult>
|
fun getUploadCapabilities(): Call<GetUploadCapabilitiesResult>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the versions
|
||||||
|
*/
|
||||||
|
@GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions")
|
||||||
|
fun getVersions(): Call<Unit>
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.session.homeserver
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.failure.Failure
|
||||||
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class HomeServerPinger @Inject constructor(private val taskExecutor: TaskExecutor,
|
||||||
|
private val capabilitiesAPI: CapabilitiesAPI) {
|
||||||
|
|
||||||
|
fun canReachHomeServer(callback: (Boolean) -> Unit) {
|
||||||
|
taskExecutor.executorScope.launch {
|
||||||
|
val canReach = canReachHomeServer()
|
||||||
|
callback(canReach)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun canReachHomeServer(): Boolean {
|
||||||
|
return try {
|
||||||
|
executeRequest<Unit>(null) {
|
||||||
|
apiCall = capabilitiesAPI.getVersions()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
if (throwable is Failure.OtherServerError) {
|
||||||
|
(throwable.httpCode == 404 || throwable.httpCode == 400)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,7 @@ import im.vector.matrix.android.api.session.events.model.EventType
|
|||||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.query.types
|
import im.vector.matrix.android.internal.database.query.whereTypes
|
||||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||||
import im.vector.matrix.android.internal.di.UserId
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
import io.realm.OrderedCollectionChangeSet
|
import io.realm.OrderedCollectionChangeSet
|
||||||
@ -42,7 +42,7 @@ internal class EventRelationsAggregationUpdater @Inject constructor(
|
|||||||
RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
||||||
|
|
||||||
override val query = Monarchy.Query<EventEntity> {
|
override val query = Monarchy.Query<EventEntity> {
|
||||||
EventEntity.types(it, listOf(
|
EventEntity.whereTypes(it, listOf(
|
||||||
EventType.MESSAGE,
|
EventType.MESSAGE,
|
||||||
EventType.REDACTION,
|
EventType.REDACTION,
|
||||||
EventType.REACTION,
|
EventType.REACTION,
|
||||||
|
@ -21,10 +21,9 @@ import im.vector.matrix.android.api.session.events.model.EventType
|
|||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomAvatarContent
|
import im.vector.matrix.android.api.session.room.model.RoomAvatarContent
|
||||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
|
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
|
||||||
import im.vector.matrix.android.internal.database.query.prev
|
import im.vector.matrix.android.internal.database.query.getOrNull
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
|
||||||
import im.vector.matrix.android.internal.di.UserId
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
|
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -40,7 +39,7 @@ internal class RoomAvatarResolver @Inject constructor(private val monarchy: Mona
|
|||||||
fun resolve(roomId: String): String? {
|
fun resolve(roomId: String): String? {
|
||||||
var res: String? = null
|
var res: String? = null
|
||||||
monarchy.doWithRealm { realm ->
|
monarchy.doWithRealm { realm ->
|
||||||
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_AVATAR).prev()
|
val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_AVATAR, stateKey = "")?.root
|
||||||
res = ContentMapper.map(roomName?.content).toModel<RoomAvatarContent>()?.avatarUrl
|
res = ContentMapper.map(roomName?.content).toModel<RoomAvatarContent>()?.avatarUrl
|
||||||
if (!res.isNullOrEmpty()) {
|
if (!res.isNullOrEmpty()) {
|
||||||
return@doWithRealm
|
return@doWithRealm
|
||||||
|
@ -59,22 +59,22 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
|
|||||||
|
|
||||||
override fun create(roomId: String): Room {
|
override fun create(roomId: String): Room {
|
||||||
return DefaultRoom(
|
return DefaultRoom(
|
||||||
roomId,
|
roomId = roomId,
|
||||||
monarchy,
|
monarchy = monarchy,
|
||||||
roomSummaryMapper,
|
roomSummaryMapper = roomSummaryMapper,
|
||||||
timelineServiceFactory.create(roomId),
|
timelineService = timelineServiceFactory.create(roomId),
|
||||||
sendServiceFactory.create(roomId),
|
sendService = sendServiceFactory.create(roomId),
|
||||||
draftServiceFactory.create(roomId),
|
draftService = draftServiceFactory.create(roomId),
|
||||||
stateServiceFactory.create(roomId),
|
stateService = stateServiceFactory.create(roomId),
|
||||||
reportingServiceFactory.create(roomId),
|
reportingService = reportingServiceFactory.create(roomId),
|
||||||
readServiceFactory.create(roomId),
|
readService = readServiceFactory.create(roomId),
|
||||||
typingServiceFactory.create(roomId),
|
typingService = typingServiceFactory.create(roomId),
|
||||||
cryptoService,
|
cryptoService = cryptoService,
|
||||||
relationServiceFactory.create(roomId),
|
relationService = relationServiceFactory.create(roomId),
|
||||||
membershipServiceFactory.create(roomId),
|
roomMembersService = membershipServiceFactory.create(roomId),
|
||||||
roomPushRuleServiceFactory.create(roomId),
|
roomPushRuleService = roomPushRuleServiceFactory.create(roomId),
|
||||||
taskExecutor,
|
taskExecutor = taskExecutor,
|
||||||
sendStateTask
|
sendStateTask = sendStateTask
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,9 +129,6 @@ internal abstract class RoomModule {
|
|||||||
@Binds
|
@Binds
|
||||||
abstract fun bindGetContextOfEventTask(task: DefaultGetContextOfEventTask): GetContextOfEventTask
|
abstract fun bindGetContextOfEventTask(task: DefaultGetContextOfEventTask): GetContextOfEventTask
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindClearUnlinkedEventsTask(task: DefaultClearUnlinkedEventsTask): ClearUnlinkedEventsTask
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindPaginationTask(task: DefaultPaginationTask): PaginationTask
|
abstract fun bindPaginationTask(task: DefaultPaginationTask): PaginationTask
|
||||||
|
|
||||||
|
@ -25,16 +25,17 @@ import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
|
|||||||
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
||||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||||
|
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||||
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
|
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
import im.vector.matrix.android.internal.database.query.getOrCreate
|
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||||
|
import im.vector.matrix.android.internal.database.query.getOrNull
|
||||||
import im.vector.matrix.android.internal.database.query.isEventRead
|
import im.vector.matrix.android.internal.database.query.isEventRead
|
||||||
import im.vector.matrix.android.internal.database.query.latestEvent
|
import im.vector.matrix.android.internal.database.query.latestEvent
|
||||||
import im.vector.matrix.android.internal.database.query.prev
|
import im.vector.matrix.android.internal.database.query.whereType
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
|
||||||
import im.vector.matrix.android.internal.di.UserId
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
import im.vector.matrix.android.internal.session.room.membership.RoomDisplayNameResolver
|
import im.vector.matrix.android.internal.session.room.membership.RoomDisplayNameResolver
|
||||||
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
|
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
|
||||||
@ -50,8 +51,9 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||||||
private val roomAvatarResolver: RoomAvatarResolver,
|
private val roomAvatarResolver: RoomAvatarResolver,
|
||||||
private val monarchy: Monarchy) {
|
private val monarchy: Monarchy) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
// TODO: maybe allow user of SDK to give that list
|
// TODO: maybe allow user of SDK to give that list
|
||||||
private val PREVIEWABLE_TYPES = listOf(
|
val PREVIEWABLE_TYPES = listOf(
|
||||||
// TODO filter message type (KEY_VERIFICATION_READY, etc.)
|
// TODO filter message type (KEY_VERIFICATION_READY, etc.)
|
||||||
EventType.MESSAGE,
|
EventType.MESSAGE,
|
||||||
EventType.STATE_ROOM_NAME,
|
EventType.STATE_ROOM_NAME,
|
||||||
@ -67,6 +69,7 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||||||
EventType.STICKER,
|
EventType.STICKER,
|
||||||
EventType.STATE_ROOM_CREATE
|
EventType.STATE_ROOM_CREATE
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun update(realm: Realm,
|
fun update(realm: Realm,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
@ -96,12 +99,15 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES)
|
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES)
|
||||||
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()
|
|
||||||
val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev()
|
val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root
|
||||||
val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()
|
val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root
|
||||||
val encryptionEvent = EventEntity.where(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
|
val lastAliasesEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root
|
||||||
|
|
||||||
|
// Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room
|
||||||
|
val encryptionEvent = EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
|
||||||
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
|
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
|
||||||
.prev()
|
.findFirst()
|
||||||
|
|
||||||
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
|
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
|
||||||
// avoid this call if we are sure there are unread events
|
// avoid this call if we are sure there are unread events
|
||||||
|
@ -120,7 +120,7 @@ internal class DefaultCreateRoomTask @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun setReadMarkers(roomId: String) {
|
private suspend fun setReadMarkers(roomId: String) {
|
||||||
val setReadMarkerParams = SetReadMarkersTask.Params(roomId, markAllAsRead = true)
|
val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadReceipt = true, forceReadMarker = true)
|
||||||
return readMarkersTask.execute(setReadMarkerParams)
|
return readMarkersTask.execute(setReadMarkerParams)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ import im.vector.matrix.android.internal.database.awaitTransaction
|
|||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.query.types
|
import im.vector.matrix.android.internal.database.query.whereTypes
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||||
import io.realm.OrderedCollectionChangeSet
|
import io.realm.OrderedCollectionChangeSet
|
||||||
@ -41,7 +41,7 @@ internal class RoomCreateEventLiveObserver @Inject constructor(@SessionDatabase
|
|||||||
: RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
: RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
||||||
|
|
||||||
override val query = Monarchy.Query<EventEntity> {
|
override val query = Monarchy.Query<EventEntity> {
|
||||||
EventEntity.types(it, listOf(EventType.STATE_ROOM_CREATE))
|
EventEntity.whereTypes(it, listOf(EventType.STATE_ROOM_CREATE))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||||
|
@ -17,23 +17,20 @@
|
|||||||
package im.vector.matrix.android.internal.session.room.draft
|
package im.vector.matrix.android.internal.session.room.draft
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Transformations
|
|
||||||
import com.squareup.inject.assisted.Assisted
|
import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.BuildConfig
|
|
||||||
import im.vector.matrix.android.api.session.room.send.DraftService
|
import im.vector.matrix.android.api.session.room.send.DraftService
|
||||||
import im.vector.matrix.android.api.session.room.send.UserDraft
|
import im.vector.matrix.android.api.session.room.send.UserDraft
|
||||||
import im.vector.matrix.android.internal.database.mapper.DraftMapper
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
import im.vector.matrix.android.internal.database.model.DraftEntity
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
import im.vector.matrix.android.internal.task.launchToCallback
|
||||||
import im.vector.matrix.android.internal.database.model.UserDraftsEntity
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
|
||||||
import io.realm.kotlin.createObject
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String,
|
internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String,
|
||||||
private val monarchy: Monarchy
|
private val draftRepository: DraftRepository,
|
||||||
|
private val taskExecutor: TaskExecutor,
|
||||||
|
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||||
) : DraftService {
|
) : DraftService {
|
||||||
|
|
||||||
@AssistedInject.Factory
|
@AssistedInject.Factory
|
||||||
@ -45,121 +42,19 @@ internal class DefaultDraftService @AssistedInject constructor(@Assisted private
|
|||||||
* The draft stack can contain several drafts. Depending of the draft to save, it will update the top draft, or create a new draft,
|
* The draft stack can contain several drafts. Depending of the draft to save, it will update the top draft, or create a new draft,
|
||||||
* or even move an existing draft to the top of the list
|
* or even move an existing draft to the top of the list
|
||||||
*/
|
*/
|
||||||
override fun saveDraft(draft: UserDraft) {
|
override fun saveDraft(draft: UserDraft, callback: MatrixCallback<Unit>): Cancelable {
|
||||||
Timber.d("Draft: saveDraft ${privacySafe(draft)}")
|
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
|
||||||
|
draftRepository.saveDraft(roomId, draft)
|
||||||
monarchy.writeAsync { realm ->
|
|
||||||
|
|
||||||
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
|
|
||||||
?: realm.createObject(roomId)
|
|
||||||
|
|
||||||
val userDraftsEntity = roomSummaryEntity.userDrafts
|
|
||||||
?: realm.createObject<UserDraftsEntity>().also {
|
|
||||||
roomSummaryEntity.userDrafts = it
|
|
||||||
}
|
|
||||||
|
|
||||||
userDraftsEntity.let { userDraftEntity ->
|
|
||||||
// Save only valid draft
|
|
||||||
if (draft.isValid()) {
|
|
||||||
// Add a new draft or update the current one?
|
|
||||||
val newDraft = DraftMapper.map(draft)
|
|
||||||
|
|
||||||
// Is it an update of the top draft?
|
|
||||||
val topDraft = userDraftEntity.userDrafts.lastOrNull()
|
|
||||||
|
|
||||||
if (topDraft == null) {
|
|
||||||
Timber.d("Draft: create a new draft ${privacySafe(draft)}")
|
|
||||||
userDraftEntity.userDrafts.add(newDraft)
|
|
||||||
} else if (topDraft.draftMode == DraftEntity.MODE_EDIT) {
|
|
||||||
// top draft is an edit
|
|
||||||
if (newDraft.draftMode == DraftEntity.MODE_EDIT) {
|
|
||||||
if (topDraft.linkedEventId == newDraft.linkedEventId) {
|
|
||||||
// Update the top draft
|
|
||||||
Timber.d("Draft: update the top edit draft ${privacySafe(draft)}")
|
|
||||||
topDraft.content = newDraft.content
|
|
||||||
} else {
|
|
||||||
// Check a previously EDIT draft with the same id
|
|
||||||
val existingEditDraftOfSameEvent = userDraftEntity.userDrafts.find {
|
|
||||||
it.draftMode == DraftEntity.MODE_EDIT && it.linkedEventId == newDraft.linkedEventId
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingEditDraftOfSameEvent != null) {
|
|
||||||
// Ignore the new text, restore what was typed before, by putting the draft to the top
|
|
||||||
Timber.d("Draft: restore a previously edit draft ${privacySafe(draft)}")
|
|
||||||
userDraftEntity.userDrafts.remove(existingEditDraftOfSameEvent)
|
|
||||||
userDraftEntity.userDrafts.add(existingEditDraftOfSameEvent)
|
|
||||||
} else {
|
|
||||||
Timber.d("Draft: add a new edit draft ${privacySafe(draft)}")
|
|
||||||
userDraftEntity.userDrafts.add(newDraft)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Add a new regular draft to the top
|
|
||||||
Timber.d("Draft: add a new draft ${privacySafe(draft)}")
|
|
||||||
userDraftEntity.userDrafts.add(newDraft)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Top draft is not an edit
|
|
||||||
if (newDraft.draftMode == DraftEntity.MODE_EDIT) {
|
|
||||||
Timber.d("Draft: create a new edit draft ${privacySafe(draft)}")
|
|
||||||
userDraftEntity.userDrafts.add(newDraft)
|
|
||||||
} else {
|
|
||||||
// Update the top draft
|
|
||||||
Timber.d("Draft: update the top draft ${privacySafe(draft)}")
|
|
||||||
topDraft.draftMode = newDraft.draftMode
|
|
||||||
topDraft.content = newDraft.content
|
|
||||||
topDraft.linkedEventId = newDraft.linkedEventId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// There is no draft to save, so the composer was clear
|
|
||||||
Timber.d("Draft: delete a draft")
|
|
||||||
|
|
||||||
val topDraft = userDraftEntity.userDrafts.lastOrNull()
|
|
||||||
|
|
||||||
if (topDraft == null) {
|
|
||||||
Timber.d("Draft: nothing to do")
|
|
||||||
} else {
|
|
||||||
// Remove the top draft
|
|
||||||
Timber.d("Draft: remove the top draft")
|
|
||||||
userDraftEntity.userDrafts.remove(topDraft)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun privacySafe(o: Any): Any {
|
override fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable {
|
||||||
if (BuildConfig.LOG_PRIVATE_DATA) {
|
return taskExecutor.executorScope.launchToCallback(coroutineDispatchers.main, callback) {
|
||||||
return o
|
draftRepository.deleteDraft(roomId)
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deleteDraft() {
|
|
||||||
Timber.d("Draft: deleteDraft()")
|
|
||||||
|
|
||||||
monarchy.writeAsync { realm ->
|
|
||||||
UserDraftsEntity.where(realm, roomId).findFirst()?.let { userDraftsEntity ->
|
|
||||||
if (userDraftsEntity.userDrafts.isNotEmpty()) {
|
|
||||||
userDraftsEntity.userDrafts.removeAt(userDraftsEntity.userDrafts.size - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDraftsLive(): LiveData<List<UserDraft>> {
|
override fun getDraftsLive(): LiveData<List<UserDraft>> {
|
||||||
val liveData = monarchy.findAllMappedWithChanges(
|
return draftRepository.getDraftsLive(roomId)
|
||||||
{ UserDraftsEntity.where(it, roomId) },
|
|
||||||
{
|
|
||||||
it.userDrafts.map { draft ->
|
|
||||||
DraftMapper.map(draft)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return Transformations.map(liveData) {
|
|
||||||
it.firstOrNull() ?: emptyList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.session.room.draft
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Transformations
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import im.vector.matrix.android.BuildConfig
|
||||||
|
import im.vector.matrix.android.api.session.room.send.UserDraft
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.DraftMapper
|
||||||
|
import im.vector.matrix.android.internal.database.model.DraftEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.UserDraftsEntity
|
||||||
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.kotlin.createObject
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DraftRepository @Inject constructor(private val monarchy: Monarchy) {
|
||||||
|
|
||||||
|
suspend fun saveDraft(roomId: String, userDraft: UserDraft) {
|
||||||
|
monarchy.awaitTransaction {
|
||||||
|
saveDraft(it, userDraft, roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteDraft(roomId: String) {
|
||||||
|
monarchy.awaitTransaction {
|
||||||
|
deleteDraft(it, roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteDraft(realm: Realm, roomId: String) {
|
||||||
|
UserDraftsEntity.where(realm, roomId).findFirst()?.let { userDraftsEntity ->
|
||||||
|
if (userDraftsEntity.userDrafts.isNotEmpty()) {
|
||||||
|
userDraftsEntity.userDrafts.removeAt(userDraftsEntity.userDrafts.size - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveDraft(realm: Realm, draft: UserDraft, roomId: String) {
|
||||||
|
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||||
|
?: realm.createObject(roomId)
|
||||||
|
|
||||||
|
val userDraftsEntity = roomSummaryEntity.userDrafts
|
||||||
|
?: realm.createObject<UserDraftsEntity>().also {
|
||||||
|
roomSummaryEntity.userDrafts = it
|
||||||
|
}
|
||||||
|
|
||||||
|
userDraftsEntity.let { userDraftEntity ->
|
||||||
|
// Save only valid draft
|
||||||
|
if (draft.isValid()) {
|
||||||
|
// Add a new draft or update the current one?
|
||||||
|
val newDraft = DraftMapper.map(draft)
|
||||||
|
|
||||||
|
// Is it an update of the top draft?
|
||||||
|
val topDraft = userDraftEntity.userDrafts.lastOrNull()
|
||||||
|
|
||||||
|
if (topDraft == null) {
|
||||||
|
Timber.d("Draft: create a new draft ${privacySafe(draft)}")
|
||||||
|
userDraftEntity.userDrafts.add(newDraft)
|
||||||
|
} else if (topDraft.draftMode == DraftEntity.MODE_EDIT) {
|
||||||
|
// top draft is an edit
|
||||||
|
if (newDraft.draftMode == DraftEntity.MODE_EDIT) {
|
||||||
|
if (topDraft.linkedEventId == newDraft.linkedEventId) {
|
||||||
|
// Update the top draft
|
||||||
|
Timber.d("Draft: update the top edit draft ${privacySafe(draft)}")
|
||||||
|
topDraft.content = newDraft.content
|
||||||
|
} else {
|
||||||
|
// Check a previously EDIT draft with the same id
|
||||||
|
val existingEditDraftOfSameEvent = userDraftEntity.userDrafts.find {
|
||||||
|
it.draftMode == DraftEntity.MODE_EDIT && it.linkedEventId == newDraft.linkedEventId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingEditDraftOfSameEvent != null) {
|
||||||
|
// Ignore the new text, restore what was typed before, by putting the draft to the top
|
||||||
|
Timber.d("Draft: restore a previously edit draft ${privacySafe(draft)}")
|
||||||
|
userDraftEntity.userDrafts.remove(existingEditDraftOfSameEvent)
|
||||||
|
userDraftEntity.userDrafts.add(existingEditDraftOfSameEvent)
|
||||||
|
} else {
|
||||||
|
Timber.d("Draft: add a new edit draft ${privacySafe(draft)}")
|
||||||
|
userDraftEntity.userDrafts.add(newDraft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add a new regular draft to the top
|
||||||
|
Timber.d("Draft: add a new draft ${privacySafe(draft)}")
|
||||||
|
userDraftEntity.userDrafts.add(newDraft)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Top draft is not an edit
|
||||||
|
if (newDraft.draftMode == DraftEntity.MODE_EDIT) {
|
||||||
|
Timber.d("Draft: create a new edit draft ${privacySafe(draft)}")
|
||||||
|
userDraftEntity.userDrafts.add(newDraft)
|
||||||
|
} else {
|
||||||
|
// Update the top draft
|
||||||
|
Timber.d("Draft: update the top draft ${privacySafe(draft)}")
|
||||||
|
topDraft.draftMode = newDraft.draftMode
|
||||||
|
topDraft.content = newDraft.content
|
||||||
|
topDraft.linkedEventId = newDraft.linkedEventId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// There is no draft to save, so the composer was clear
|
||||||
|
Timber.d("Draft: delete a draft")
|
||||||
|
|
||||||
|
val topDraft = userDraftEntity.userDrafts.lastOrNull()
|
||||||
|
|
||||||
|
if (topDraft == null) {
|
||||||
|
Timber.d("Draft: nothing to do")
|
||||||
|
} else {
|
||||||
|
// Remove the top draft
|
||||||
|
Timber.d("Draft: remove the top draft")
|
||||||
|
userDraftEntity.userDrafts.remove(topDraft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDraftsLive(roomId: String): LiveData<List<UserDraft>> {
|
||||||
|
val liveData = monarchy.findAllMappedWithChanges(
|
||||||
|
{ UserDraftsEntity.where(it, roomId) },
|
||||||
|
{
|
||||||
|
it.userDrafts.map { draft ->
|
||||||
|
DraftMapper.map(draft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return Transformations.map(liveData) {
|
||||||
|
it.firstOrNull() ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun privacySafe(o: Any): Any {
|
||||||
|
if (BuildConfig.LOG_PRIVATE_DATA) {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
@ -18,9 +18,11 @@ package im.vector.matrix.android.internal.session.room.membership
|
|||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
import im.vector.matrix.android.internal.database.helper.TimelineEventSenderVisitor
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import im.vector.matrix.android.internal.database.helper.addStateEvent
|
import im.vector.matrix.android.internal.database.mapper.toEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
|
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.network.executeRequest
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||||
@ -47,7 +49,6 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
|
|||||||
private val syncTokenStore: SyncTokenStore,
|
private val syncTokenStore: SyncTokenStore,
|
||||||
private val roomSummaryUpdater: RoomSummaryUpdater,
|
private val roomSummaryUpdater: RoomSummaryUpdater,
|
||||||
private val roomMemberEventHandler: RoomMemberEventHandler,
|
private val roomMemberEventHandler: RoomMemberEventHandler,
|
||||||
private val timelineEventSenderVisitor: TimelineEventSenderVisitor,
|
|
||||||
private val eventBus: EventBus
|
private val eventBus: EventBus
|
||||||
) : LoadRoomMembersTask {
|
) : LoadRoomMembersTask {
|
||||||
|
|
||||||
@ -69,12 +70,17 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
|
|||||||
?: realm.createObject(roomId)
|
?: realm.createObject(roomId)
|
||||||
|
|
||||||
for (roomMemberEvent in response.roomMemberEvents) {
|
for (roomMemberEvent in response.roomMemberEvents) {
|
||||||
roomEntity.addStateEvent(roomMemberEvent)
|
if (roomMemberEvent.eventId == null || roomMemberEvent.stateKey == null) {
|
||||||
roomMemberEventHandler.handle(realm, roomId, roomMemberEvent)
|
continue
|
||||||
}
|
}
|
||||||
timelineEventSenderVisitor.clear()
|
val eventEntity = roomMemberEvent.toEntity(roomId, SendState.SYNCED).let {
|
||||||
roomEntity.chunks.flatMap { it.timelineEvents }.forEach {
|
realm.copyToRealmOrUpdate(it)
|
||||||
timelineEventSenderVisitor.visit(it)
|
}
|
||||||
|
CurrentStateEventEntity.getOrCreate(realm, roomId, roomMemberEvent.stateKey, roomMemberEvent.type).apply {
|
||||||
|
eventId = roomMemberEvent.eventId
|
||||||
|
root = eventEntity
|
||||||
|
}
|
||||||
|
roomMemberEventHandler.handle(realm, roomId, roomMemberEvent)
|
||||||
}
|
}
|
||||||
roomEntity.areAllMembersLoaded = true
|
roomEntity.areAllMembersLoaded = true
|
||||||
roomSummaryUpdater.update(realm, roomId, updateMembers = true)
|
roomSummaryUpdater.update(realm, roomId, updateMembers = true)
|
||||||
|
@ -21,14 +21,17 @@ import com.zhuinden.monarchy.Monarchy
|
|||||||
import im.vector.matrix.android.R
|
import im.vector.matrix.android.R
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.*
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomNameContent
|
||||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||||
import im.vector.matrix.android.internal.database.model.*
|
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity
|
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.query.prev
|
import im.vector.matrix.android.internal.database.query.getOrNull
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.di.UserId
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -57,19 +60,19 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
|
|||||||
var name: CharSequence? = null
|
var name: CharSequence? = null
|
||||||
monarchy.doWithRealm { realm ->
|
monarchy.doWithRealm { realm ->
|
||||||
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
|
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
|
||||||
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_NAME).prev()
|
val roomName = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root
|
||||||
name = ContentMapper.map(roomName?.content).toModel<RoomNameContent>()?.name
|
name = ContentMapper.map(roomName?.content).toModel<RoomNameContent>()?.name
|
||||||
if (!name.isNullOrEmpty()) {
|
if (!name.isNullOrEmpty()) {
|
||||||
return@doWithRealm
|
return@doWithRealm
|
||||||
}
|
}
|
||||||
|
|
||||||
val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev()
|
val canonicalAlias = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root
|
||||||
name = ContentMapper.map(canonicalAlias?.content).toModel<RoomCanonicalAliasContent>()?.canonicalAlias
|
name = ContentMapper.map(canonicalAlias?.content).toModel<RoomCanonicalAliasContent>()?.canonicalAlias
|
||||||
if (!name.isNullOrEmpty()) {
|
if (!name.isNullOrEmpty()) {
|
||||||
return@doWithRealm
|
return@doWithRealm
|
||||||
}
|
}
|
||||||
|
|
||||||
val aliases = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()
|
val aliases = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root
|
||||||
name = ContentMapper.map(aliases?.content).toModel<RoomAliasesContent>()?.aliases?.firstOrNull()
|
name = ContentMapper.map(aliases?.content).toModel<RoomAliasesContent>()?.aliases?.firstOrNull()
|
||||||
if (!name.isNullOrEmpty()) {
|
if (!name.isNullOrEmpty()) {
|
||||||
return@doWithRealm
|
return@doWithRealm
|
||||||
|
@ -18,14 +18,15 @@ package im.vector.matrix.android.internal.session.room.membership
|
|||||||
|
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
import im.vector.matrix.android.internal.database.model.*
|
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity
|
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.query.getOrNull
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmQuery
|
import io.realm.RealmQuery
|
||||||
import io.realm.Sort
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is an helper around STATE_ROOM_MEMBER events.
|
* This class is an helper around STATE_ROOM_MEMBER events.
|
||||||
@ -41,11 +42,7 @@ internal class RoomMemberHelper(private val realm: Realm,
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getLastStateEvent(userId: String): EventEntity? {
|
fun getLastStateEvent(userId: String): EventEntity? {
|
||||||
return EventEntity
|
return CurrentStateEventEntity.getOrNull(realm, roomId, userId, EventType.STATE_ROOM_MEMBER)?.root
|
||||||
.where(realm, roomId, EventType.STATE_ROOM_MEMBER)
|
|
||||||
.equalTo(EventEntityFields.STATE_KEY, userId)
|
|
||||||
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
|
|
||||||
.findFirst()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLastRoomMember(userId: String): RoomMemberSummaryEntity? {
|
fun getLastRoomMember(userId: String): RoomMemberSummaryEntity? {
|
||||||
@ -58,10 +55,8 @@ internal class RoomMemberHelper(private val realm: Realm,
|
|||||||
if (displayName.isNullOrEmpty()) {
|
if (displayName.isNullOrEmpty()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return EventEntity
|
return RoomMemberSummaryEntity.where(realm, roomId)
|
||||||
.where(realm, roomId, EventType.STATE_ROOM_MEMBER)
|
.equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, displayName)
|
||||||
.contains(EventEntityFields.CONTENT, "\"displayname\":\"$displayName\"")
|
|
||||||
.distinct(EventEntityFields.STATE_KEY)
|
|
||||||
.findAll()
|
.findAll()
|
||||||
.size == 1
|
.size == 1
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ internal class DefaultJoinRoomTask @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun setReadMarkers(roomId: String) {
|
private suspend fun setReadMarkers(roomId: String) {
|
||||||
val setReadMarkerParams = SetReadMarkersTask.Params(roomId, markAllAsRead = true)
|
val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadMarker = true, forceReadReceipt = true)
|
||||||
readMarkersTask.execute(setReadMarkerParams)
|
readMarkersTask.execute(setReadMarkerParams)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ import im.vector.matrix.android.api.session.events.model.EventType
|
|||||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.query.types
|
import im.vector.matrix.android.internal.database.query.whereTypes
|
||||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||||
import io.realm.OrderedCollectionChangeSet
|
import io.realm.OrderedCollectionChangeSet
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
@ -38,7 +38,7 @@ internal class EventsPruner @Inject constructor(@SessionDatabase realmConfigurat
|
|||||||
private val pruneEventTask: PruneEventTask) :
|
private val pruneEventTask: PruneEventTask) :
|
||||||
RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
||||||
|
|
||||||
override val query = Monarchy.Query<EventEntity> { EventEntity.types(it, listOf(EventType.REDACTION)) }
|
override val query = Monarchy.Query<EventEntity> { EventEntity.whereTypes(it, listOf(EventType.REDACTION)) }
|
||||||
|
|
||||||
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||||
Timber.v("Event pruner called with ${changeSet.insertions.size} insertions")
|
Timber.v("Event pruner called with ${changeSet.insertions.size} insertions")
|
||||||
|
@ -20,7 +20,6 @@ import im.vector.matrix.android.api.session.events.model.Event
|
|||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||||
import im.vector.matrix.android.api.session.events.model.UnsignedData
|
import im.vector.matrix.android.api.session.events.model.UnsignedData
|
||||||
import im.vector.matrix.android.internal.database.helper.TimelineEventSenderVisitor
|
|
||||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||||
import im.vector.matrix.android.internal.database.mapper.EventMapper
|
import im.vector.matrix.android.internal.database.mapper.EventMapper
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
@ -41,8 +40,7 @@ internal interface PruneEventTask : Task<PruneEventTask.Params, Unit> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class DefaultPruneEventTask @Inject constructor(private val monarchy: Monarchy,
|
internal class DefaultPruneEventTask @Inject constructor(private val monarchy: Monarchy) : PruneEventTask {
|
||||||
private val timelineEventSenderVisitor: TimelineEventSenderVisitor) : PruneEventTask {
|
|
||||||
|
|
||||||
override suspend fun execute(params: PruneEventTask.Params) {
|
override suspend fun execute(params: PruneEventTask.Params) {
|
||||||
monarchy.awaitTransaction { realm ->
|
monarchy.awaitTransaction { realm ->
|
||||||
@ -98,9 +96,11 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (typeToPrune == EventType.STATE_ROOM_MEMBER && stateKey != null) {
|
if (typeToPrune == EventType.STATE_ROOM_MEMBER && stateKey != null) {
|
||||||
timelineEventSenderVisitor.clear(roomId = eventToPrune.roomId, senderId = stateKey)
|
TimelineEventEntity.findWithSenderMembershipEvent(realm, eventToPrune.eventId).forEach {
|
||||||
val timelineEventsToUpdate = TimelineEventEntity.findWithSenderMembershipEvent(realm, eventToPrune.eventId)
|
it.senderName = null
|
||||||
timelineEventSenderVisitor.visit(timelineEventsToUpdate)
|
it.isUniqueDisplayName = false
|
||||||
|
it.senderAvatar = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,10 +50,14 @@ internal class DefaultReadService @AssistedInject constructor(
|
|||||||
fun create(roomId: String): ReadService
|
fun create(roomId: String): ReadService
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun markAllAsRead(callback: MatrixCallback<Unit>) {
|
override fun markAsRead(params: ReadService.MarkAsReadParams, callback: MatrixCallback<Unit>) {
|
||||||
val params = SetReadMarkersTask.Params(roomId, markAllAsRead = true)
|
val taskParams = SetReadMarkersTask.Params(
|
||||||
|
roomId = roomId,
|
||||||
|
forceReadMarker = params.forceReadMarker(),
|
||||||
|
forceReadReceipt = params.forceReadReceipt()
|
||||||
|
)
|
||||||
setReadMarkersTask
|
setReadMarkersTask
|
||||||
.configureWith(params) {
|
.configureWith(taskParams) {
|
||||||
this.callback = callback
|
this.callback = callback
|
||||||
}
|
}
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
@ -110,4 +114,12 @@ internal class DefaultReadService @AssistedInject constructor(
|
|||||||
it.firstOrNull() ?: emptyList()
|
it.firstOrNull() ?: emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ReadService.MarkAsReadParams.forceReadMarker(): Boolean {
|
||||||
|
return this == ReadService.MarkAsReadParams.READ_MARKER || this == ReadService.MarkAsReadParams.BOTH
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ReadService.MarkAsReadParams.forceReadReceipt(): Boolean {
|
||||||
|
return this == ReadService.MarkAsReadParams.READ_RECEIPT || this == ReadService.MarkAsReadParams.BOTH
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ internal class DefaultMarkAllRoomsReadTask @Inject constructor(private val readM
|
|||||||
|
|
||||||
override suspend fun execute(params: MarkAllRoomsReadTask.Params) {
|
override suspend fun execute(params: MarkAllRoomsReadTask.Params) {
|
||||||
params.roomIds.forEach { roomId ->
|
params.roomIds.forEach { roomId ->
|
||||||
readMarkersTask.execute(SetReadMarkersTask.Params(roomId, markAllAsRead = true))
|
readMarkersTask.execute(SetReadMarkersTask.Params(roomId, forceReadMarker = true, forceReadReceipt = true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,16 +35,16 @@ import io.realm.Realm
|
|||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.collections.HashMap
|
|
||||||
import kotlin.collections.set
|
import kotlin.collections.set
|
||||||
|
|
||||||
internal interface SetReadMarkersTask : Task<SetReadMarkersTask.Params, Unit> {
|
internal interface SetReadMarkersTask : Task<SetReadMarkersTask.Params, Unit> {
|
||||||
|
|
||||||
data class Params(
|
data class Params(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val markAllAsRead: Boolean = false,
|
|
||||||
val fullyReadEventId: String? = null,
|
val fullyReadEventId: String? = null,
|
||||||
val readReceiptEventId: String? = null
|
val readReceiptEventId: String? = null,
|
||||||
|
val forceReadReceipt: Boolean = false,
|
||||||
|
val forceReadMarker: Boolean = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,17 +62,18 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun execute(params: SetReadMarkersTask.Params) {
|
override suspend fun execute(params: SetReadMarkersTask.Params) {
|
||||||
val markers = HashMap<String, String>()
|
val markers = HashMap<String, String>()
|
||||||
|
|
||||||
Timber.v("Execute set read marker with params: $params")
|
Timber.v("Execute set read marker with params: $params")
|
||||||
val (fullyReadEventId, readReceiptEventId) = if (params.markAllAsRead) {
|
val latestSyncedEventId = latestSyncedEventId(params.roomId)
|
||||||
val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
val fullyReadEventId = if (params.forceReadMarker) {
|
||||||
TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId
|
latestSyncedEventId
|
||||||
}
|
|
||||||
Pair(latestSyncedEventId, latestSyncedEventId)
|
|
||||||
} else {
|
} else {
|
||||||
Pair(params.fullyReadEventId, params.readReceiptEventId)
|
params.fullyReadEventId
|
||||||
|
}
|
||||||
|
val readReceiptEventId = if (params.forceReadReceipt) {
|
||||||
|
latestSyncedEventId
|
||||||
|
} else {
|
||||||
|
params.readReceiptEventId
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy, params.roomId, fullyReadEventId)) {
|
if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy, params.roomId, fullyReadEventId)) {
|
||||||
if (LocalEcho.isLocalEchoId(fullyReadEventId)) {
|
if (LocalEcho.isLocalEchoId(fullyReadEventId)) {
|
||||||
Timber.w("Can't set read marker for local event $fullyReadEventId")
|
Timber.w("Can't set read marker for local event $fullyReadEventId")
|
||||||
@ -80,7 +81,6 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
|
|||||||
markers[READ_MARKER] = fullyReadEventId
|
markers[READ_MARKER] = fullyReadEventId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (readReceiptEventId != null
|
if (readReceiptEventId != null
|
||||||
&& !isEventRead(monarchy, userId, params.roomId, readReceiptEventId)) {
|
&& !isEventRead(monarchy, userId, params.roomId, readReceiptEventId)) {
|
||||||
if (LocalEcho.isLocalEchoId(readReceiptEventId)) {
|
if (LocalEcho.isLocalEchoId(readReceiptEventId)) {
|
||||||
@ -89,16 +89,25 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
|
|||||||
markers[READ_RECEIPT] = readReceiptEventId
|
markers[READ_RECEIPT] = readReceiptEventId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (markers.isEmpty()) {
|
|
||||||
return
|
val shouldUpdateRoomSummary = readReceiptEventId != null && readReceiptEventId == latestSyncedEventId
|
||||||
|
if (markers.isNotEmpty() || shouldUpdateRoomSummary) {
|
||||||
|
updateDatabase(params.roomId, markers, shouldUpdateRoomSummary)
|
||||||
}
|
}
|
||||||
updateDatabase(params.roomId, markers)
|
if (markers.isNotEmpty()) {
|
||||||
executeRequest<Unit>(eventBus) {
|
executeRequest<Unit>(eventBus) {
|
||||||
|
isRetryable = true
|
||||||
apiCall = roomAPI.sendReadMarker(params.roomId, markers)
|
apiCall = roomAPI.sendReadMarker(params.roomId, markers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun updateDatabase(roomId: String, markers: HashMap<String, String>) {
|
private fun latestSyncedEventId(roomId: String): String? =
|
||||||
|
Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||||
|
TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateDatabase(roomId: String, markers: HashMap<String, String>, shouldUpdateRoomSummary: Boolean) {
|
||||||
monarchy.awaitTransaction { realm ->
|
monarchy.awaitTransaction { realm ->
|
||||||
val readMarkerId = markers[READ_MARKER]
|
val readMarkerId = markers[READ_MARKER]
|
||||||
val readReceiptId = markers[READ_RECEIPT]
|
val readReceiptId = markers[READ_RECEIPT]
|
||||||
@ -108,8 +117,8 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
|
|||||||
if (readReceiptId != null) {
|
if (readReceiptId != null) {
|
||||||
val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId)
|
val readReceiptContent = ReadReceiptHandler.createContent(userId, readReceiptId)
|
||||||
readReceiptHandler.handle(realm, roomId, readReceiptContent, false)
|
readReceiptHandler.handle(realm, roomId, readReceiptContent, false)
|
||||||
val isLatestReceived = TimelineEventEntity.latestEvent(realm, roomId = roomId, includesSending = false)?.eventId == readReceiptId
|
}
|
||||||
if (isLatestReceived) {
|
if (shouldUpdateRoomSummary) {
|
||||||
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||||
?: return@awaitTransaction
|
?: return@awaitTransaction
|
||||||
roomSummary.notificationCount = 0
|
roomSummary.notificationCount = 0
|
||||||
@ -118,5 +127,4 @@ internal class DefaultSetReadMarkersTask @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -233,6 +233,6 @@ internal class DefaultRelationService @AssistedInject constructor(
|
|||||||
* the same transaction id is received (in unsigned data)
|
* the same transaction id is received (in unsigned data)
|
||||||
*/
|
*/
|
||||||
private fun saveLocalEcho(event: Event) {
|
private fun saveLocalEcho(event: Event) {
|
||||||
eventFactory.saveLocalEcho(monarchy, event)
|
eventFactory.createLocalEcho(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,28 +25,24 @@ import com.squareup.inject.assisted.AssistedInject
|
|||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
import im.vector.matrix.android.api.session.events.model.*
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
import im.vector.matrix.android.api.session.events.model.isImageMessage
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
import im.vector.matrix.android.api.session.events.model.isTextMessage
|
||||||
import im.vector.matrix.android.api.session.room.send.SendService
|
import im.vector.matrix.android.api.session.room.send.SendService
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
import im.vector.matrix.android.api.util.CancelableBag
|
import im.vector.matrix.android.api.util.CancelableBag
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
|
||||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
|
||||||
import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates
|
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
|
||||||
import im.vector.matrix.android.internal.di.SessionId
|
import im.vector.matrix.android.internal.di.SessionId
|
||||||
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
||||||
import im.vector.matrix.android.internal.session.content.UploadContentWorker
|
import im.vector.matrix.android.internal.session.content.UploadContentWorker
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon
|
import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon
|
||||||
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.util.CancelableWork
|
import im.vector.matrix.android.internal.util.CancelableWork
|
||||||
import im.vector.matrix.android.internal.worker.AlwaysSuccessfulWorker
|
import im.vector.matrix.android.internal.worker.AlwaysSuccessfulWorker
|
||||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
import im.vector.matrix.android.internal.worker.startChain
|
import im.vector.matrix.android.internal.worker.startChain
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -61,7 +57,9 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
@SessionId private val sessionId: String,
|
@SessionId private val sessionId: String,
|
||||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||||
private val cryptoService: CryptoService,
|
private val cryptoService: CryptoService,
|
||||||
private val monarchy: Monarchy
|
private val monarchy: Monarchy,
|
||||||
|
private val taskExecutor: TaskExecutor,
|
||||||
|
private val localEchoRepository: LocalEchoRepository
|
||||||
) : SendService {
|
) : SendService {
|
||||||
|
|
||||||
@AssistedInject.Factory
|
@AssistedInject.Factory
|
||||||
@ -73,15 +71,14 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
|
|
||||||
override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable {
|
override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable {
|
||||||
val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also {
|
val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also {
|
||||||
saveLocalEcho(it)
|
createLocalEcho(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendEvent(event)
|
return sendEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable {
|
override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable {
|
||||||
val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also {
|
val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also {
|
||||||
saveLocalEcho(it)
|
createLocalEcho(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendEvent(event)
|
return sendEvent(event)
|
||||||
@ -156,13 +153,8 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteFailedEcho(localEcho: TimelineEvent) {
|
override fun deleteFailedEcho(localEcho: TimelineEvent) {
|
||||||
monarchy.writeAsync { realm ->
|
taskExecutor.executorScope.launch {
|
||||||
TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "")
|
localEchoRepository.deleteFailedEcho(roomId, localEcho)
|
||||||
.findFirst()
|
|
||||||
?.let { it.deleteFromRealm() }
|
|
||||||
EventEntity.where(realm, eventId = localEcho.root.eventId ?: "")
|
|
||||||
.findFirst()
|
|
||||||
?.let { it.deleteFromRealm() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,67 +172,26 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it)
|
.beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it)
|
||||||
.enqueue()
|
.enqueue()
|
||||||
}
|
}
|
||||||
|
taskExecutor.executorScope.launch {
|
||||||
monarchy.writeAsync { realm ->
|
localEchoRepository.clearSendingQueue(roomId)
|
||||||
RoomEntity.where(realm, roomId).findFirst()?.let { room ->
|
|
||||||
room.sendingTimelineEvents.forEach {
|
|
||||||
it.root?.sendState = SendState.UNDELIVERED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resendAllFailedMessages() {
|
override fun resendAllFailedMessages() {
|
||||||
monarchy.writeAsync { realm ->
|
taskExecutor.executorScope.launch {
|
||||||
TimelineEventEntity
|
val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId)
|
||||||
.findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES)
|
eventsToResend.forEach {
|
||||||
.sortedBy { it.root?.originServerTs ?: 0 }
|
sendEvent(it)
|
||||||
.forEach { timelineEventEntity ->
|
|
||||||
timelineEventEntity.root?.let {
|
|
||||||
val event = it.asDomain()
|
|
||||||
when (event.getClearType()) {
|
|
||||||
EventType.MESSAGE,
|
|
||||||
EventType.REDACTION,
|
|
||||||
EventType.REACTION -> {
|
|
||||||
val content = event.getClearContent().toModel<MessageContent>()
|
|
||||||
if (content != null) {
|
|
||||||
when (content.type) {
|
|
||||||
MessageType.MSGTYPE_EMOTE,
|
|
||||||
MessageType.MSGTYPE_NOTICE,
|
|
||||||
MessageType.MSGTYPE_LOCATION,
|
|
||||||
MessageType.MSGTYPE_TEXT -> {
|
|
||||||
it.sendState = SendState.UNSENT
|
|
||||||
sendEvent(event)
|
|
||||||
}
|
|
||||||
MessageType.MSGTYPE_FILE,
|
|
||||||
MessageType.MSGTYPE_VIDEO,
|
|
||||||
MessageType.MSGTYPE_IMAGE,
|
|
||||||
MessageType.MSGTYPE_AUDIO -> {
|
|
||||||
// need to resend the attachement
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Timber.e("Cannot resend message ${event.type} / ${content.type}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.e("Unsupported message to resend ${event.type}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Timber.e("Unsupported message to resend ${event.type}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendMedia(attachment: ContentAttachmentData): Cancelable {
|
override fun sendMedia(attachment: ContentAttachmentData): Cancelable {
|
||||||
// Create an event with the media file path
|
// Create an event with the media file path
|
||||||
val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also {
|
val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also {
|
||||||
saveLocalEcho(it)
|
createLocalEcho(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
return internalSendMedia(event, attachment)
|
return internalSendMedia(event, attachment)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,8 +226,8 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
return CancelableWork(workManagerProvider.workManager, sendWork.id)
|
return CancelableWork(workManagerProvider.workManager, sendWork.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveLocalEcho(event: Event) {
|
private fun createLocalEcho(event: Event) {
|
||||||
localEchoEventFactory.saveLocalEcho(monarchy, event)
|
localEchoEventFactory.createLocalEcho(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildWorkName(identifier: String): String {
|
private fun buildWorkName(identifier: String): String {
|
||||||
@ -305,7 +256,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
|
|
||||||
private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest {
|
private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest {
|
||||||
val redactEvent = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason).also {
|
val redactEvent = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason).also {
|
||||||
saveLocalEcho(it)
|
createLocalEcho(it)
|
||||||
}
|
}
|
||||||
val sendContentWorkerParams = RedactEventWorker.Params(sessionId, redactEvent.eventId!!, roomId, event.eventId, reason)
|
val sendContentWorkerParams = RedactEventWorker.Params(sessionId, redactEvent.eventId!!, roomId, event.eventId, reason)
|
||||||
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||||
@ -326,4 +277,4 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,26 +18,43 @@ package im.vector.matrix.android.internal.session.room.send
|
|||||||
|
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import com.zhuinden.monarchy.Monarchy
|
|
||||||
import im.vector.matrix.android.R
|
import im.vector.matrix.android.R
|
||||||
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
import im.vector.matrix.android.api.session.events.model.*
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.room.model.message.*
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||||
|
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.UnsignedData
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toContent
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.AudioInfo
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.FileInfo
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.ImageInfo
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageFormat
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.ThumbnailInfo
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.VideoInfo
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.isReply
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
|
import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo
|
import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
|
import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
||||||
import im.vector.matrix.android.internal.database.helper.addSendingEvent
|
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
|
||||||
import im.vector.matrix.android.internal.di.UserId
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
|
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
|
||||||
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
|
||||||
import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils
|
import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils
|
||||||
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.util.StringProvider
|
import im.vector.matrix.android.internal.util.StringProvider
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.commonmark.parser.Parser
|
import org.commonmark.parser.Parser
|
||||||
import org.commonmark.renderer.html.HtmlRenderer
|
import org.commonmark.renderer.html.HtmlRenderer
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -54,8 +71,9 @@ import javax.inject.Inject
|
|||||||
internal class LocalEchoEventFactory @Inject constructor(
|
internal class LocalEchoEventFactory @Inject constructor(
|
||||||
@UserId private val userId: String,
|
@UserId private val userId: String,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val roomSummaryUpdater: RoomSummaryUpdater,
|
private val textPillsUtils: TextPillsUtils,
|
||||||
private val textPillsUtils: TextPillsUtils
|
private val taskExecutor: TaskExecutor,
|
||||||
|
private val localEchoRepository: LocalEchoRepository
|
||||||
) {
|
) {
|
||||||
// TODO Inject
|
// TODO Inject
|
||||||
private val parser = Parser.builder().build()
|
private val parser = Parser.builder().build()
|
||||||
@ -66,7 +84,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) {
|
if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) {
|
||||||
return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType)
|
return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType)
|
||||||
}
|
}
|
||||||
val content = MessageTextContent(type = msgType, body = text.toString())
|
val content = MessageTextContent(msgType = msgType, body = text.toString())
|
||||||
return createEvent(roomId, content)
|
return createEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +123,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
compatibilityText: String): Event {
|
compatibilityText: String): Event {
|
||||||
return createEvent(roomId,
|
return createEvent(roomId,
|
||||||
MessageTextContent(
|
MessageTextContent(
|
||||||
type = msgType,
|
msgType = msgType,
|
||||||
body = compatibilityText,
|
body = compatibilityText,
|
||||||
relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId),
|
relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId),
|
||||||
newContent = createTextContent(newBodyText, newBodyAutoMarkdown)
|
newContent = createTextContent(newBodyText, newBodyAutoMarkdown)
|
||||||
@ -114,7 +132,8 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createReplaceTextOfReply(roomId: String, eventReplaced: TimelineEvent,
|
fun createReplaceTextOfReply(roomId: String,
|
||||||
|
eventReplaced: TimelineEvent,
|
||||||
originalEvent: TimelineEvent,
|
originalEvent: TimelineEvent,
|
||||||
newBodyText: String,
|
newBodyText: String,
|
||||||
newBodyAutoMarkdown: Boolean,
|
newBodyAutoMarkdown: Boolean,
|
||||||
@ -140,12 +159,12 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
|
|
||||||
return createEvent(roomId,
|
return createEvent(roomId,
|
||||||
MessageTextContent(
|
MessageTextContent(
|
||||||
type = msgType,
|
msgType = msgType,
|
||||||
body = compatibilityText,
|
body = compatibilityText,
|
||||||
relatesTo = RelationDefaultContent(RelationType.REPLACE, eventReplaced.root.eventId),
|
relatesTo = RelationDefaultContent(RelationType.REPLACE, eventReplaced.root.eventId),
|
||||||
newContent = MessageTextContent(
|
newContent = MessageTextContent(
|
||||||
type = msgType,
|
msgType = msgType,
|
||||||
format = MessageType.FORMAT_MATRIX_HTML,
|
format = MessageFormat.FORMAT_MATRIX_HTML,
|
||||||
body = replyFallback,
|
body = replyFallback,
|
||||||
formattedBody = replyFormatted
|
formattedBody = replyFormatted
|
||||||
)
|
)
|
||||||
@ -197,7 +216,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val content = MessageImageContent(
|
val content = MessageImageContent(
|
||||||
type = MessageType.MSGTYPE_IMAGE,
|
msgType = MessageType.MSGTYPE_IMAGE,
|
||||||
body = attachment.name ?: "image",
|
body = attachment.name ?: "image",
|
||||||
info = ImageInfo(
|
info = ImageInfo(
|
||||||
mimeType = attachment.mimeType,
|
mimeType = attachment.mimeType,
|
||||||
@ -229,7 +248,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
val content = MessageVideoContent(
|
val content = MessageVideoContent(
|
||||||
type = MessageType.MSGTYPE_VIDEO,
|
msgType = MessageType.MSGTYPE_VIDEO,
|
||||||
body = attachment.name ?: "video",
|
body = attachment.name ?: "video",
|
||||||
videoInfo = VideoInfo(
|
videoInfo = VideoInfo(
|
||||||
mimeType = attachment.mimeType,
|
mimeType = attachment.mimeType,
|
||||||
@ -248,7 +267,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
|
|
||||||
private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
||||||
val content = MessageAudioContent(
|
val content = MessageAudioContent(
|
||||||
type = MessageType.MSGTYPE_AUDIO,
|
msgType = MessageType.MSGTYPE_AUDIO,
|
||||||
body = attachment.name ?: "audio",
|
body = attachment.name ?: "audio",
|
||||||
audioInfo = AudioInfo(
|
audioInfo = AudioInfo(
|
||||||
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
|
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
|
||||||
@ -261,7 +280,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
|
|
||||||
private fun createFileEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
private fun createFileEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
||||||
val content = MessageFileContent(
|
val content = MessageFileContent(
|
||||||
type = MessageType.MSGTYPE_FILE,
|
msgType = MessageType.MSGTYPE_FILE,
|
||||||
body = attachment.name ?: "file",
|
body = attachment.name ?: "file",
|
||||||
info = FileInfo(
|
info = FileInfo(
|
||||||
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() }
|
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() }
|
||||||
@ -332,8 +351,8 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
|
|
||||||
val eventId = eventReplied.root.eventId ?: return null
|
val eventId = eventReplied.root.eventId ?: return null
|
||||||
val content = MessageTextContent(
|
val content = MessageTextContent(
|
||||||
type = MessageType.MSGTYPE_TEXT,
|
msgType = MessageType.MSGTYPE_TEXT,
|
||||||
format = MessageType.FORMAT_MATRIX_HTML,
|
format = MessageFormat.FORMAT_MATRIX_HTML,
|
||||||
body = replyFallback,
|
body = replyFallback,
|
||||||
formattedBody = replyFormatted,
|
formattedBody = replyFormatted,
|
||||||
relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId))
|
relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId))
|
||||||
@ -366,13 +385,13 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
* himself a reply, but it will contain the fallbacks, so we have to trim them.
|
* himself a reply, but it will contain the fallbacks, so we have to trim them.
|
||||||
*/
|
*/
|
||||||
private fun bodyForReply(content: MessageContent?, originalContent: MessageContent?): TextContent {
|
private fun bodyForReply(content: MessageContent?, originalContent: MessageContent?): TextContent {
|
||||||
when (content?.type) {
|
when (content?.msgType) {
|
||||||
MessageType.MSGTYPE_EMOTE,
|
MessageType.MSGTYPE_EMOTE,
|
||||||
MessageType.MSGTYPE_TEXT,
|
MessageType.MSGTYPE_TEXT,
|
||||||
MessageType.MSGTYPE_NOTICE -> {
|
MessageType.MSGTYPE_NOTICE -> {
|
||||||
var formattedText: String? = null
|
var formattedText: String? = null
|
||||||
if (content is MessageTextContent) {
|
if (content is MessageTextContent) {
|
||||||
if (content.format == MessageType.FORMAT_MATRIX_HTML) {
|
if (content.format == MessageFormat.FORMAT_MATRIX_HTML) {
|
||||||
formattedText = content.formattedBody
|
formattedText = content.formattedBody
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -421,13 +440,10 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveLocalEcho(monarchy: Monarchy, event: Event) {
|
fun createLocalEcho(event: Event) {
|
||||||
checkNotNull(event.roomId) { "Your event should have a roomId" }
|
checkNotNull(event.roomId) { "Your event should have a roomId" }
|
||||||
monarchy.writeAsync { realm ->
|
taskExecutor.executorScope.launch {
|
||||||
val roomEntity = RoomEntity.where(realm, roomId = event.roomId).findFirst()
|
localEchoRepository.createLocalEcho(event)
|
||||||
?: return@writeAsync
|
|
||||||
roomEntity.addSendingEvent(event)
|
|
||||||
roomSummaryUpdater.update(realm, event.roomId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.session.room.send
|
||||||
|
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||||
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.matrix.android.internal.database.helper.nextId
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.toEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
|
import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates
|
||||||
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||||
|
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
|
||||||
|
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
|
||||||
|
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||||
|
import io.realm.Realm
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.lang.IllegalStateException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class LocalEchoRepository @Inject constructor(private val monarchy: Monarchy,
|
||||||
|
private val roomSummaryUpdater: RoomSummaryUpdater,
|
||||||
|
private val eventBus: EventBus,
|
||||||
|
private val timelineEventMapper: TimelineEventMapper) {
|
||||||
|
|
||||||
|
suspend fun createLocalEcho(event: Event) {
|
||||||
|
val roomId = event.roomId ?: throw IllegalStateException("You should have set a roomId for your event")
|
||||||
|
val senderId = event.senderId ?: throw IllegalStateException("You should have set a senderIf for your event")
|
||||||
|
if (event.eventId == null) {
|
||||||
|
throw IllegalStateException("You should have set an eventId for your event")
|
||||||
|
}
|
||||||
|
val timelineEventEntity = Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||||
|
val eventEntity = event.toEntity(roomId, SendState.UNSENT)
|
||||||
|
val roomMemberHelper = RoomMemberHelper(realm, roomId)
|
||||||
|
val myUser = roomMemberHelper.getLastRoomMember(senderId)
|
||||||
|
val localId = TimelineEventEntity.nextId(realm)
|
||||||
|
TimelineEventEntity(localId).also {
|
||||||
|
it.root = eventEntity
|
||||||
|
it.eventId = event.eventId
|
||||||
|
it.roomId = roomId
|
||||||
|
it.senderName = myUser?.displayName
|
||||||
|
it.senderAvatar = myUser?.avatarUrl
|
||||||
|
it.isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(myUser?.displayName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val timelineEvent = timelineEventMapper.map(timelineEventEntity)
|
||||||
|
eventBus.post(DefaultTimeline.OnLocalEchoCreated(roomId = roomId, timelineEvent = timelineEvent))
|
||||||
|
monarchy.awaitTransaction { realm ->
|
||||||
|
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@awaitTransaction
|
||||||
|
roomEntity.sendingTimelineEvents.add(0, timelineEventEntity)
|
||||||
|
roomSummaryUpdater.update(realm, roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteFailedEcho(roomId: String, localEcho: TimelineEvent) {
|
||||||
|
monarchy.awaitTransaction { realm ->
|
||||||
|
TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm()
|
||||||
|
EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun clearSendingQueue(roomId: String) {
|
||||||
|
monarchy.awaitTransaction { realm ->
|
||||||
|
RoomEntity.where(realm, roomId).findFirst()?.let { room ->
|
||||||
|
room.sendingTimelineEvents.forEach {
|
||||||
|
it.root?.sendState = SendState.UNDELIVERED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateSendState(roomId: String, eventIds: List<String>, sendState: SendState) {
|
||||||
|
monarchy.awaitTransaction { realm ->
|
||||||
|
val timelineEvents = TimelineEventEntity.where(realm, roomId, eventIds).findAll()
|
||||||
|
timelineEvents.forEach {
|
||||||
|
it.root?.sendState = sendState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllFailedEventsToResend(roomId: String): List<Event> {
|
||||||
|
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||||
|
TimelineEventEntity
|
||||||
|
.findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES)
|
||||||
|
.sortedByDescending { it.displayIndex }
|
||||||
|
.mapNotNull { it.root?.asDomain() }
|
||||||
|
.filter { event ->
|
||||||
|
when (event.getClearType()) {
|
||||||
|
EventType.MESSAGE,
|
||||||
|
EventType.REDACTION,
|
||||||
|
EventType.REACTION -> {
|
||||||
|
val content = event.getClearContent().toModel<MessageContent>()
|
||||||
|
if (content != null) {
|
||||||
|
when (content.msgType) {
|
||||||
|
MessageType.MSGTYPE_EMOTE,
|
||||||
|
MessageType.MSGTYPE_NOTICE,
|
||||||
|
MessageType.MSGTYPE_LOCATION,
|
||||||
|
MessageType.MSGTYPE_TEXT -> {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MessageType.MSGTYPE_FILE,
|
||||||
|
MessageType.MSGTYPE_VIDEO,
|
||||||
|
MessageType.MSGTYPE_IMAGE,
|
||||||
|
MessageType.MSGTYPE_AUDIO -> {
|
||||||
|
// need to resend the attachment
|
||||||
|
false
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Timber.e("Cannot resend message ${event.type} / ${content.msgType}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.e("Unsupported message to resend ${event.type}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Timber.e("Unsupported message to resend ${event.type}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.session.room.send
|
package im.vector.matrix.android.internal.session.room.send
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageFormat
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||||
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromHtmlReply
|
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromHtmlReply
|
||||||
@ -33,8 +34,8 @@ data class TextContent(
|
|||||||
|
|
||||||
fun TextContent.toMessageTextContent(msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent {
|
fun TextContent.toMessageTextContent(msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent {
|
||||||
return MessageTextContent(
|
return MessageTextContent(
|
||||||
type = msgType,
|
msgType = msgType,
|
||||||
format = MessageType.FORMAT_MATRIX_HTML.takeIf { formattedText != null },
|
format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null },
|
||||||
body = text,
|
body = text,
|
||||||
formattedBody = formattedText
|
formattedBody = formattedText
|
||||||
)
|
)
|
||||||
|
@ -28,10 +28,9 @@ import im.vector.matrix.android.api.session.room.state.StateService
|
|||||||
import im.vector.matrix.android.api.util.Optional
|
import im.vector.matrix.android.api.util.Optional
|
||||||
import im.vector.matrix.android.api.util.toOptional
|
import im.vector.matrix.android.api.util.toOptional
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
|
||||||
import im.vector.matrix.android.internal.database.query.descending
|
import im.vector.matrix.android.internal.database.query.getOrNull
|
||||||
import im.vector.matrix.android.internal.database.query.prev
|
import im.vector.matrix.android.internal.database.query.whereStateKey
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
@ -47,16 +46,16 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
|
|||||||
fun create(roomId: String): StateService
|
fun create(roomId: String): StateService
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStateEvent(eventType: String): Event? {
|
override fun getStateEvent(eventType: String, stateKey: String): Event? {
|
||||||
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||||
EventEntity.where(realm, roomId, eventType).prev()?.asDomain()
|
CurrentStateEventEntity.getOrNull(realm, roomId, type = eventType, stateKey = stateKey)?.root?.asDomain()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStateEventLive(eventType: String): LiveData<Optional<Event>> {
|
override fun getStateEventLive(eventType: String, stateKey: String): LiveData<Optional<Event>> {
|
||||||
val liveData = monarchy.findAllMappedWithChanges(
|
val liveData = monarchy.findAllMappedWithChanges(
|
||||||
{ realm -> EventEntity.where(realm, roomId, eventType).descending() },
|
{ realm -> CurrentStateEventEntity.whereStateKey(realm, roomId, type = eventType, stateKey = "") },
|
||||||
{ it.asDomain() }
|
{ it.root?.asDomain() }
|
||||||
)
|
)
|
||||||
return Transformations.map(liveData) { results ->
|
return Transformations.map(liveData) { results ->
|
||||||
results.firstOrNull().toOptional()
|
results.firstOrNull().toOptional()
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
* Copyright 2019 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
|
|
||||||
*/
|
|
||||||
package im.vector.matrix.android.internal.session.room.timeline
|
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
|
||||||
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
|
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
|
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
|
||||||
import im.vector.matrix.android.internal.task.Task
|
|
||||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal interface ClearUnlinkedEventsTask : Task<ClearUnlinkedEventsTask.Params, Unit> {
|
|
||||||
|
|
||||||
data class Params(val roomId: String)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DefaultClearUnlinkedEventsTask @Inject constructor(private val monarchy: Monarchy) : ClearUnlinkedEventsTask {
|
|
||||||
|
|
||||||
override suspend fun execute(params: ClearUnlinkedEventsTask.Params) {
|
|
||||||
monarchy.awaitTransaction { localRealm ->
|
|
||||||
val unlinkedChunks = ChunkEntity
|
|
||||||
.where(localRealm, roomId = params.roomId)
|
|
||||||
.equalTo(ChunkEntityFields.IS_UNLINKED, true)
|
|
||||||
.findAll()
|
|
||||||
unlinkedChunks.forEach {
|
|
||||||
it.deleteOnCascade()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -27,8 +27,7 @@ internal interface GetContextOfEventTask : Task<GetContextOfEventTask.Params, To
|
|||||||
|
|
||||||
data class Params(
|
data class Params(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val eventId: String,
|
val eventId: String
|
||||||
val limit: Int
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,8 +41,9 @@ internal class DefaultGetContextOfEventTask @Inject constructor(
|
|||||||
override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result {
|
override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result {
|
||||||
val filter = filterRepository.getRoomFilter()
|
val filter = filterRepository.getRoomFilter()
|
||||||
val response = executeRequest<EventContextResponse>(eventBus) {
|
val response = executeRequest<EventContextResponse>(eventBus) {
|
||||||
apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, params.limit, filter)
|
// We are limiting the response to the event with eventId to be sure we don't have any issue with potential merging process.
|
||||||
|
apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter)
|
||||||
}
|
}
|
||||||
return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS)
|
return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.FORWARDS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,7 @@ internal class DefaultPaginationTask @Inject constructor(
|
|||||||
override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result {
|
override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result {
|
||||||
val filter = filterRepository.getRoomFilter()
|
val filter = filterRepository.getRoomFilter()
|
||||||
val chunk = executeRequest<PaginationResponse>(eventBus) {
|
val chunk = executeRequest<PaginationResponse>(eventBus) {
|
||||||
|
isRetryable = true
|
||||||
apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter)
|
apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter)
|
||||||
}
|
}
|
||||||
return tokenChunkEventPersistor.insertInDb(chunk, params.roomId, params.direction)
|
return tokenChunkEventPersistor.insertInDb(chunk, params.roomId, params.direction)
|
||||||
|
@ -19,7 +19,10 @@ package im.vector.matrix.android.internal.session.room.timeline
|
|||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
@ -27,27 +30,39 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
|||||||
import im.vector.matrix.android.api.util.CancelableBag
|
import im.vector.matrix.android.api.util.CancelableBag
|
||||||
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
|
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.*
|
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
|
||||||
import im.vector.matrix.android.internal.database.query.FilterContent
|
import im.vector.matrix.android.internal.database.query.FilterContent
|
||||||
import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates
|
import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.database.query.whereInRoom
|
import im.vector.matrix.android.internal.database.query.whereInRoom
|
||||||
import im.vector.matrix.android.internal.task.TaskConstraints
|
import im.vector.matrix.android.internal.database.query.whereRoomId
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
import im.vector.matrix.android.internal.util.Debouncer
|
import im.vector.matrix.android.internal.util.Debouncer
|
||||||
import im.vector.matrix.android.internal.util.createBackgroundHandler
|
import im.vector.matrix.android.internal.util.createBackgroundHandler
|
||||||
import im.vector.matrix.android.internal.util.createUIHandler
|
import im.vector.matrix.android.internal.util.createUIHandler
|
||||||
import io.realm.*
|
import io.realm.OrderedCollectionChangeSet
|
||||||
|
import io.realm.OrderedRealmCollectionChangeListener
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmConfiguration
|
||||||
|
import io.realm.RealmQuery
|
||||||
|
import io.realm.RealmResults
|
||||||
|
import io.realm.Sort
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import org.greenrobot.eventbus.Subscribe
|
||||||
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
import java.util.Collections
|
||||||
|
import java.util.UUID
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
import kotlin.collections.HashMap
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
private const val MIN_FETCHING_COUNT = 30
|
private const val MIN_FETCHING_COUNT = 30
|
||||||
|
|
||||||
@ -57,14 +72,17 @@ internal class DefaultTimeline(
|
|||||||
private val realmConfiguration: RealmConfiguration,
|
private val realmConfiguration: RealmConfiguration,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val contextOfEventTask: GetContextOfEventTask,
|
private val contextOfEventTask: GetContextOfEventTask,
|
||||||
private val clearUnlinkedEventsTask: ClearUnlinkedEventsTask,
|
|
||||||
private val paginationTask: PaginationTask,
|
private val paginationTask: PaginationTask,
|
||||||
private val cryptoService: CryptoService,
|
private val cryptoService: CryptoService,
|
||||||
private val timelineEventMapper: TimelineEventMapper,
|
private val timelineEventMapper: TimelineEventMapper,
|
||||||
private val settings: TimelineSettings,
|
private val settings: TimelineSettings,
|
||||||
private val hiddenReadReceipts: TimelineHiddenReadReceipts
|
private val hiddenReadReceipts: TimelineHiddenReadReceipts,
|
||||||
|
private val eventBus: EventBus
|
||||||
) : Timeline, TimelineHiddenReadReceipts.Delegate {
|
) : Timeline, TimelineHiddenReadReceipts.Delegate {
|
||||||
|
|
||||||
|
data class OnNewTimelineEvents(val roomId: String, val eventIds: List<String>)
|
||||||
|
data class OnLocalEchoCreated(val roomId: String, val timelineEvent: TimelineEvent)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
|
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
|
||||||
}
|
}
|
||||||
@ -85,6 +103,7 @@ internal class DefaultTimeline(
|
|||||||
|
|
||||||
private var prevDisplayIndex: Int? = null
|
private var prevDisplayIndex: Int? = null
|
||||||
private var nextDisplayIndex: Int? = null
|
private var nextDisplayIndex: Int? = null
|
||||||
|
private val inMemorySendingEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
||||||
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
||||||
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
|
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
|
||||||
private val backwardsState = AtomicReference(State())
|
private val backwardsState = AtomicReference(State())
|
||||||
@ -101,7 +120,7 @@ internal class DefaultTimeline(
|
|||||||
if (!results.isLoaded || !results.isValid) {
|
if (!results.isLoaded || !results.isValid) {
|
||||||
return@OrderedRealmCollectionChangeListener
|
return@OrderedRealmCollectionChangeListener
|
||||||
}
|
}
|
||||||
handleUpdates(changeSet)
|
handleUpdates(results, changeSet)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val relationsListener = OrderedRealmCollectionChangeListener<RealmResults<EventAnnotationsSummaryEntity>> { collection, changeSet ->
|
private val relationsListener = OrderedRealmCollectionChangeListener<RealmResults<EventAnnotationsSummaryEntity>> { collection, changeSet ->
|
||||||
@ -118,7 +137,7 @@ internal class DefaultTimeline(
|
|||||||
if (hasChange) postSnapshot()
|
if (hasChange) postSnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public methods ******************************************************************************
|
// Public methods ******************************************************************************
|
||||||
|
|
||||||
override fun paginate(direction: Timeline.Direction, count: Int) {
|
override fun paginate(direction: Timeline.Direction, count: Int) {
|
||||||
BACKGROUND_HANDLER.post {
|
BACKGROUND_HANDLER.post {
|
||||||
@ -149,6 +168,7 @@ internal class DefaultTimeline(
|
|||||||
override fun start() {
|
override fun start() {
|
||||||
if (isStarted.compareAndSet(false, true)) {
|
if (isStarted.compareAndSet(false, true)) {
|
||||||
Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
|
Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
|
||||||
|
eventBus.register(this)
|
||||||
BACKGROUND_HANDLER.post {
|
BACKGROUND_HANDLER.post {
|
||||||
eventDecryptor.start()
|
eventDecryptor.start()
|
||||||
val realm = Realm.getInstance(realmConfiguration)
|
val realm = Realm.getInstance(realmConfiguration)
|
||||||
@ -160,18 +180,18 @@ internal class DefaultTimeline(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING).findAll()
|
nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
|
||||||
filteredEvents = nonFilteredEvents.where()
|
filteredEvents = nonFilteredEvents.where()
|
||||||
.filterEventsWithSettings()
|
.filterEventsWithSettings()
|
||||||
.findAll()
|
.findAll()
|
||||||
handleInitialLoad()
|
handleInitialLoad()
|
||||||
filteredEvents.addChangeListener(eventsChangeListener)
|
nonFilteredEvents.addChangeListener(eventsChangeListener)
|
||||||
|
|
||||||
eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId)
|
eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId)
|
||||||
.findAllAsync()
|
.findAllAsync()
|
||||||
.also { it.addChangeListener(relationsListener) }
|
.also { it.addChangeListener(relationsListener) }
|
||||||
|
|
||||||
if (settings.buildReadReceipts) {
|
if (settings.shouldHandleHiddenReadReceipts()) {
|
||||||
hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this)
|
hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this)
|
||||||
}
|
}
|
||||||
isReady.set(true)
|
isReady.set(true)
|
||||||
@ -179,9 +199,14 @@ internal class DefaultTimeline(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean {
|
||||||
|
return buildReadReceipts && (filterEdits || filterTypes)
|
||||||
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
if (isStarted.compareAndSet(true, false)) {
|
if (isStarted.compareAndSet(true, false)) {
|
||||||
isReady.set(false)
|
isReady.set(false)
|
||||||
|
eventBus.unregister(this)
|
||||||
Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId")
|
Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId")
|
||||||
cancelableBag.cancel()
|
cancelableBag.cancel()
|
||||||
BACKGROUND_HANDLER.removeCallbacksAndMessages(null)
|
BACKGROUND_HANDLER.removeCallbacksAndMessages(null)
|
||||||
@ -190,10 +215,10 @@ internal class DefaultTimeline(
|
|||||||
if (this::eventRelations.isInitialized) {
|
if (this::eventRelations.isInitialized) {
|
||||||
eventRelations.removeAllChangeListeners()
|
eventRelations.removeAllChangeListeners()
|
||||||
}
|
}
|
||||||
if (this::filteredEvents.isInitialized) {
|
if (this::nonFilteredEvents.isInitialized) {
|
||||||
filteredEvents.removeAllChangeListeners()
|
nonFilteredEvents.removeAllChangeListeners()
|
||||||
}
|
}
|
||||||
if (settings.buildReadReceipts) {
|
if (settings.shouldHandleHiddenReadReceipts()) {
|
||||||
hiddenReadReceipts.dispose()
|
hiddenReadReceipts.dispose()
|
||||||
}
|
}
|
||||||
clearAllValues()
|
clearAllValues()
|
||||||
@ -202,9 +227,6 @@ internal class DefaultTimeline(
|
|||||||
}
|
}
|
||||||
eventDecryptor.destroy()
|
eventDecryptor.destroy()
|
||||||
}
|
}
|
||||||
clearUnlinkedEventsTask
|
|
||||||
.configureWith(ClearUnlinkedEventsTask.Params(roomId))
|
|
||||||
.executeBy(taskExecutor)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,7 +260,7 @@ internal class DefaultTimeline(
|
|||||||
// Otherwise, we should check if the event is in the db, but is hidden because of filters
|
// Otherwise, we should check if the event is in the db, but is hidden because of filters
|
||||||
return Realm.getInstance(realmConfiguration).use { localRealm ->
|
return Realm.getInstance(realmConfiguration).use { localRealm ->
|
||||||
val nonFilteredEvents = buildEventQuery(localRealm)
|
val nonFilteredEvents = buildEventQuery(localRealm)
|
||||||
.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
|
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
.findAll()
|
.findAll()
|
||||||
|
|
||||||
val nonFilteredEvent = nonFilteredEvents.where()
|
val nonFilteredEvent = nonFilteredEvents.where()
|
||||||
@ -253,11 +275,11 @@ internal class DefaultTimeline(
|
|||||||
.findFirst() == null
|
.findFirst() == null
|
||||||
|
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
val displayIndex = nonFilteredEvent?.root?.displayIndex
|
val displayIndex = nonFilteredEvent?.displayIndex
|
||||||
if (displayIndex != null) {
|
if (displayIndex != null) {
|
||||||
// Then we are looking for the first displayable event after the hidden one
|
// Then we are looking for the first displayable event after the hidden one
|
||||||
val firstDisplayedEvent = filteredEvents.where()
|
val firstDisplayedEvent = filteredEvents.where()
|
||||||
.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex)
|
.lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, displayIndex)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
firstDisplayedEvent?.eventId
|
firstDisplayedEvent?.eventId
|
||||||
} else {
|
} else {
|
||||||
@ -302,6 +324,26 @@ internal class DefaultTimeline(
|
|||||||
postSnapshot()
|
postSnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||||
|
fun onNewTimelineEvents(onNewTimelineEvents: OnNewTimelineEvents) {
|
||||||
|
if (isLive && onNewTimelineEvents.roomId == roomId) {
|
||||||
|
listeners.forEach {
|
||||||
|
it.onNewTimelineEvents(onNewTimelineEvents.eventIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||||
|
fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated) {
|
||||||
|
if (isLive && onLocalEchoCreated.roomId == roomId) {
|
||||||
|
listeners.forEach {
|
||||||
|
it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId))
|
||||||
|
}
|
||||||
|
inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent)
|
||||||
|
postSnapshot()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Private methods *****************************************************************************
|
// Private methods *****************************************************************************
|
||||||
|
|
||||||
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
|
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
|
||||||
@ -327,14 +369,14 @@ internal class DefaultTimeline(
|
|||||||
|
|
||||||
updateState(Timeline.Direction.FORWARDS) {
|
updateState(Timeline.Direction.FORWARDS) {
|
||||||
it.copy(
|
it.copy(
|
||||||
hasMoreInCache = firstBuiltEvent == null || firstBuiltEvent.displayIndex < firstCacheEvent?.root?.displayIndex ?: Int.MIN_VALUE,
|
hasMoreInCache = firstBuiltEvent == null || firstBuiltEvent.displayIndex < firstCacheEvent?.displayIndex ?: Int.MIN_VALUE,
|
||||||
hasReachedEnd = chunkEntity?.isLastForward ?: false
|
hasReachedEnd = chunkEntity?.isLastForward ?: false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateState(Timeline.Direction.BACKWARDS) {
|
updateState(Timeline.Direction.BACKWARDS) {
|
||||||
it.copy(
|
it.copy(
|
||||||
hasMoreInCache = lastBuiltEvent == null || lastBuiltEvent.displayIndex > lastCacheEvent?.root?.displayIndex ?: Int.MAX_VALUE,
|
hasMoreInCache = lastBuiltEvent == null || lastBuiltEvent.displayIndex > lastCacheEvent?.displayIndex ?: Int.MAX_VALUE,
|
||||||
hasReachedEnd = chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE
|
hasReachedEnd = chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -346,10 +388,9 @@ internal class DefaultTimeline(
|
|||||||
*/
|
*/
|
||||||
private fun paginateInternal(startDisplayIndex: Int?,
|
private fun paginateInternal(startDisplayIndex: Int?,
|
||||||
direction: Timeline.Direction,
|
direction: Timeline.Direction,
|
||||||
count: Int,
|
count: Int): Boolean {
|
||||||
strict: Boolean = false): Boolean {
|
|
||||||
updateState(direction) { it.copy(requestedPaginationCount = count, isPaginating = true) }
|
updateState(direction) { it.copy(requestedPaginationCount = count, isPaginating = true) }
|
||||||
val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong(), strict)
|
val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong())
|
||||||
val shouldFetchMore = builtCount < count && !hasReachedEnd(direction)
|
val shouldFetchMore = builtCount < count && !hasReachedEnd(direction)
|
||||||
if (shouldFetchMore) {
|
if (shouldFetchMore) {
|
||||||
val newRequestedCount = count - builtCount
|
val newRequestedCount = count - builtCount
|
||||||
@ -359,7 +400,6 @@ internal class DefaultTimeline(
|
|||||||
} else {
|
} else {
|
||||||
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
|
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return !shouldFetchMore
|
return !shouldFetchMore
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,12 +410,15 @@ internal class DefaultTimeline(
|
|||||||
private fun buildSendingEvents(): List<TimelineEvent> {
|
private fun buildSendingEvents(): List<TimelineEvent> {
|
||||||
val sendingEvents = ArrayList<TimelineEvent>()
|
val sendingEvents = ArrayList<TimelineEvent>()
|
||||||
if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) {
|
if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) {
|
||||||
|
sendingEvents.addAll(inMemorySendingEvents.filterEventsWithSettings())
|
||||||
roomEntity?.sendingTimelineEvents
|
roomEntity?.sendingTimelineEvents
|
||||||
?.where()
|
?.where()
|
||||||
?.filterEventsWithSettings()
|
?.filterEventsWithSettings()
|
||||||
?.findAll()
|
?.findAll()
|
||||||
?.forEach {
|
?.forEach { timelineEventEntity ->
|
||||||
sendingEvents.add(timelineEventMapper.map(it))
|
if (sendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) {
|
||||||
|
sendingEvents.add(timelineEventMapper.map(timelineEventEntity))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sendingEvents
|
return sendingEvents
|
||||||
@ -409,26 +452,26 @@ internal class DefaultTimeline(
|
|||||||
var shouldFetchInitialEvent = false
|
var shouldFetchInitialEvent = false
|
||||||
val currentInitialEventId = initialEventId
|
val currentInitialEventId = initialEventId
|
||||||
val initialDisplayIndex = if (currentInitialEventId == null) {
|
val initialDisplayIndex = if (currentInitialEventId == null) {
|
||||||
filteredEvents.firstOrNull()?.root?.displayIndex
|
nonFilteredEvents.firstOrNull()?.displayIndex
|
||||||
} else {
|
} else {
|
||||||
val initialEvent = nonFilteredEvents.where()
|
val initialEvent = nonFilteredEvents.where()
|
||||||
.equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId)
|
.equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
|
|
||||||
shouldFetchInitialEvent = initialEvent == null
|
shouldFetchInitialEvent = initialEvent == null
|
||||||
initialEvent?.root?.displayIndex
|
initialEvent?.displayIndex
|
||||||
}
|
}
|
||||||
prevDisplayIndex = initialDisplayIndex
|
prevDisplayIndex = initialDisplayIndex
|
||||||
nextDisplayIndex = initialDisplayIndex
|
nextDisplayIndex = initialDisplayIndex
|
||||||
if (currentInitialEventId != null && shouldFetchInitialEvent) {
|
if (currentInitialEventId != null && shouldFetchInitialEvent) {
|
||||||
fetchEvent(currentInitialEventId)
|
fetchEvent(currentInitialEventId)
|
||||||
} else {
|
} else {
|
||||||
val count = min(settings.initialSize, filteredEvents.size)
|
val count = filteredEvents.size.coerceAtMost(settings.initialSize)
|
||||||
if (initialEventId == null) {
|
if (initialEventId == null) {
|
||||||
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count, strict = false)
|
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count)
|
||||||
} else {
|
} else {
|
||||||
paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2, strict = false)
|
paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, (count / 2).coerceAtLeast(1))
|
||||||
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count / 2, strict = true)
|
paginateInternal(initialDisplayIndex?.minus(1), Timeline.Direction.BACKWARDS, (count / 2).coerceAtLeast(1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
postSnapshot()
|
postSnapshot()
|
||||||
@ -437,7 +480,7 @@ internal class DefaultTimeline(
|
|||||||
/**
|
/**
|
||||||
* This has to be called on TimelineThread as it access realm live results
|
* This has to be called on TimelineThread as it access realm live results
|
||||||
*/
|
*/
|
||||||
private fun handleUpdates(changeSet: OrderedCollectionChangeSet) {
|
private fun handleUpdates(results: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||||
// If changeSet has deletion we are having a gap, so we clear everything
|
// If changeSet has deletion we are having a gap, so we clear everything
|
||||||
if (changeSet.deletionRanges.isNotEmpty()) {
|
if (changeSet.deletionRanges.isNotEmpty()) {
|
||||||
clearAllValues()
|
clearAllValues()
|
||||||
@ -445,9 +488,9 @@ internal class DefaultTimeline(
|
|||||||
var postSnapshot = false
|
var postSnapshot = false
|
||||||
changeSet.insertionRanges.forEach { range ->
|
changeSet.insertionRanges.forEach { range ->
|
||||||
val (startDisplayIndex, direction) = if (range.startIndex == 0) {
|
val (startDisplayIndex, direction) = if (range.startIndex == 0) {
|
||||||
Pair(filteredEvents[range.length - 1]!!.root!!.displayIndex, Timeline.Direction.FORWARDS)
|
Pair(results[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS)
|
||||||
} else {
|
} else {
|
||||||
Pair(filteredEvents[range.startIndex]!!.root!!.displayIndex, Timeline.Direction.BACKWARDS)
|
Pair(results[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS)
|
||||||
}
|
}
|
||||||
val state = getState(direction)
|
val state = getState(direction)
|
||||||
if (state.isPaginating) {
|
if (state.isPaginating) {
|
||||||
@ -460,7 +503,7 @@ internal class DefaultTimeline(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
changeSet.changes.forEach { index ->
|
changeSet.changes.forEach { index ->
|
||||||
val eventEntity = filteredEvents[index]
|
val eventEntity = results[index]
|
||||||
eventEntity?.eventId?.let { eventId ->
|
eventEntity?.eventId?.let { eventId ->
|
||||||
postSnapshot = rebuildEvent(eventId) {
|
postSnapshot = rebuildEvent(eventId) {
|
||||||
buildTimelineEvent(eventEntity)
|
buildTimelineEvent(eventEntity)
|
||||||
@ -489,7 +532,6 @@ internal class DefaultTimeline(
|
|||||||
Timber.v("Should fetch $limit items $direction")
|
Timber.v("Should fetch $limit items $direction")
|
||||||
cancelableBag += paginationTask
|
cancelableBag += paginationTask
|
||||||
.configureWith(params) {
|
.configureWith(params) {
|
||||||
this.constraints = TaskConstraints(connectedToNetwork = true)
|
|
||||||
this.callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
|
this.callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
|
||||||
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
|
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
|
||||||
when (data) {
|
when (data) {
|
||||||
@ -530,7 +572,7 @@ internal class DefaultTimeline(
|
|||||||
* This has to be called on TimelineThread as it access realm live results
|
* This has to be called on TimelineThread as it access realm live results
|
||||||
*/
|
*/
|
||||||
private fun getLiveChunk(): ChunkEntity? {
|
private fun getLiveChunk(): ChunkEntity? {
|
||||||
return filteredEvents.firstOrNull()?.chunk?.firstOrNull()
|
return nonFilteredEvents.firstOrNull()?.chunk?.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -539,17 +581,16 @@ internal class DefaultTimeline(
|
|||||||
*/
|
*/
|
||||||
private fun buildTimelineEvents(startDisplayIndex: Int?,
|
private fun buildTimelineEvents(startDisplayIndex: Int?,
|
||||||
direction: Timeline.Direction,
|
direction: Timeline.Direction,
|
||||||
count: Long,
|
count: Long): Int {
|
||||||
strict: Boolean = false): Int {
|
|
||||||
if (count < 1 || startDisplayIndex == null) {
|
if (count < 1 || startDisplayIndex == null) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
val offsetResults = getOffsetResults(startDisplayIndex, direction, count, strict)
|
val offsetResults = getOffsetResults(startDisplayIndex, direction, count)
|
||||||
if (offsetResults.isEmpty()) {
|
if (offsetResults.isEmpty()) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
val offsetIndex = offsetResults.last()!!.root!!.displayIndex
|
val offsetIndex = offsetResults.last()!!.displayIndex
|
||||||
if (direction == Timeline.Direction.BACKWARDS) {
|
if (direction == Timeline.Direction.BACKWARDS) {
|
||||||
prevDisplayIndex = offsetIndex - 1
|
prevDisplayIndex = offsetIndex - 1
|
||||||
} else {
|
} else {
|
||||||
@ -558,6 +599,11 @@ internal class DefaultTimeline(
|
|||||||
offsetResults.forEach { eventEntity ->
|
offsetResults.forEach { eventEntity ->
|
||||||
|
|
||||||
val timelineEvent = buildTimelineEvent(eventEntity)
|
val timelineEvent = buildTimelineEvent(eventEntity)
|
||||||
|
val transactionId = timelineEvent.root.unsignedData?.transactionId
|
||||||
|
val sendingEvent = inMemorySendingEvents.find {
|
||||||
|
it.eventId == transactionId
|
||||||
|
}
|
||||||
|
inMemorySendingEvents.remove(sendingEvent)
|
||||||
|
|
||||||
if (timelineEvent.isEncrypted()
|
if (timelineEvent.isEncrypted()
|
||||||
&& timelineEvent.root.mxDecryptionResult == null) {
|
&& timelineEvent.root.mxDecryptionResult == null) {
|
||||||
@ -586,23 +632,16 @@ internal class DefaultTimeline(
|
|||||||
*/
|
*/
|
||||||
private fun getOffsetResults(startDisplayIndex: Int,
|
private fun getOffsetResults(startDisplayIndex: Int,
|
||||||
direction: Timeline.Direction,
|
direction: Timeline.Direction,
|
||||||
count: Long,
|
count: Long): RealmResults<TimelineEventEntity> {
|
||||||
strict: Boolean): RealmResults<TimelineEventEntity> {
|
|
||||||
val offsetQuery = filteredEvents.where()
|
val offsetQuery = filteredEvents.where()
|
||||||
if (direction == Timeline.Direction.BACKWARDS) {
|
if (direction == Timeline.Direction.BACKWARDS) {
|
||||||
offsetQuery.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
|
offsetQuery
|
||||||
if (strict) {
|
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
offsetQuery.lessThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
|
.lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
|
||||||
} else {
|
} else {
|
||||||
offsetQuery.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
|
offsetQuery
|
||||||
}
|
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
|
||||||
} else {
|
.greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
|
||||||
offsetQuery.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING)
|
|
||||||
if (strict) {
|
|
||||||
offsetQuery.greaterThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
|
|
||||||
} else {
|
|
||||||
offsetQuery.greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return offsetQuery
|
return offsetQuery
|
||||||
.limit(count)
|
.limit(count)
|
||||||
@ -612,19 +651,23 @@ internal class DefaultTimeline(
|
|||||||
private fun buildEventQuery(realm: Realm): RealmQuery<TimelineEventEntity> {
|
private fun buildEventQuery(realm: Realm): RealmQuery<TimelineEventEntity> {
|
||||||
return if (initialEventId == null) {
|
return if (initialEventId == null) {
|
||||||
TimelineEventEntity
|
TimelineEventEntity
|
||||||
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY)
|
.whereRoomId(realm, roomId = roomId)
|
||||||
.equalTo("${TimelineEventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST_FORWARD}", true)
|
.equalTo("${TimelineEventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST_FORWARD}", true)
|
||||||
} else {
|
} else {
|
||||||
TimelineEventEntity
|
TimelineEventEntity
|
||||||
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH)
|
.whereRoomId(realm, roomId = roomId)
|
||||||
.`in`("${TimelineEventEntityFields.CHUNK}.${ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID}", arrayOf(initialEventId))
|
.`in`("${TimelineEventEntityFields.CHUNK}.${ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID}", arrayOf(initialEventId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchEvent(eventId: String) {
|
private fun fetchEvent(eventId: String) {
|
||||||
val params = GetContextOfEventTask.Params(roomId, eventId, settings.initialSize)
|
val params = GetContextOfEventTask.Params(roomId, eventId)
|
||||||
cancelableBag += contextOfEventTask.configureWith(params) {
|
cancelableBag += contextOfEventTask.configureWith(params) {
|
||||||
callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
|
callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
|
||||||
|
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
|
||||||
|
postSnapshot()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
override fun onFailure(failure: Throwable) {
|
||||||
postFailure(failure)
|
postFailure(failure)
|
||||||
}
|
}
|
||||||
@ -645,7 +688,7 @@ internal class DefaultTimeline(
|
|||||||
it.onTimelineUpdated(snapshot)
|
it.onTimelineUpdated(snapshot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debouncer.debounce("post_snapshot", runnable, 50)
|
debouncer.debounce("post_snapshot", runnable, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -670,7 +713,7 @@ internal class DefaultTimeline(
|
|||||||
forwardsState.set(State())
|
forwardsState.set(State())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extension methods ***************************************************************************
|
// Extension methods ***************************************************************************
|
||||||
|
|
||||||
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
||||||
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
|
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
|
||||||
@ -686,6 +729,23 @@ internal class DefaultTimeline(
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun List<TimelineEvent>.filterEventsWithSettings(): List<TimelineEvent> {
|
||||||
|
return filter {
|
||||||
|
val filterType = if (settings.filterTypes) {
|
||||||
|
settings.allowedTypes.contains(it.root.type)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
val filterEdits = if (settings.filterEdits && it.root.type == EventType.MESSAGE) {
|
||||||
|
val messageContent = it.root.content.toModel<MessageContent>()
|
||||||
|
messageContent?.relatesTo?.type != RelationType.REPLACE
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
filterType && filterEdits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private data class State(
|
private data class State(
|
||||||
val hasReachedEnd: Boolean = false,
|
val hasReachedEnd: Boolean = false,
|
||||||
val hasMoreInCache: Boolean = true,
|
val hasMoreInCache: Boolean = true,
|
||||||
|
@ -34,16 +34,17 @@ import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
|||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.util.fetchCopyMap
|
import im.vector.matrix.android.internal.util.fetchCopyMap
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
|
||||||
internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String,
|
internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String,
|
||||||
private val monarchy: Monarchy,
|
private val monarchy: Monarchy,
|
||||||
|
private val eventBus: EventBus,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val contextOfEventTask: GetContextOfEventTask,
|
private val contextOfEventTask: GetContextOfEventTask,
|
||||||
private val cryptoService: CryptoService,
|
private val cryptoService: CryptoService,
|
||||||
private val paginationTask: PaginationTask,
|
private val paginationTask: PaginationTask,
|
||||||
private val timelineEventMapper: TimelineEventMapper,
|
private val timelineEventMapper: TimelineEventMapper,
|
||||||
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
|
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper
|
||||||
private val clearUnlinkedEventsTask: ClearUnlinkedEventsTask
|
|
||||||
) : TimelineService {
|
) : TimelineService {
|
||||||
|
|
||||||
@AssistedInject.Factory
|
@AssistedInject.Factory
|
||||||
@ -52,17 +53,18 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline {
|
override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline {
|
||||||
return DefaultTimeline(roomId,
|
return DefaultTimeline(
|
||||||
eventId,
|
roomId = roomId,
|
||||||
monarchy.realmConfiguration,
|
initialEventId = eventId,
|
||||||
taskExecutor,
|
realmConfiguration = monarchy.realmConfiguration,
|
||||||
contextOfEventTask,
|
taskExecutor = taskExecutor,
|
||||||
clearUnlinkedEventsTask,
|
contextOfEventTask = contextOfEventTask,
|
||||||
paginationTask,
|
paginationTask = paginationTask,
|
||||||
cryptoService,
|
cryptoService = cryptoService,
|
||||||
timelineEventMapper,
|
timelineEventMapper = timelineEventMapper,
|
||||||
settings,
|
settings = settings,
|
||||||
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings)
|
hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
|
||||||
|
eventBus = eventBus
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,7 +110,7 @@ internal class TimelineEventDecryptor(
|
|||||||
eventEntity.setDecryptionResult(result)
|
eventEntity.setDecryptionResult(result)
|
||||||
}
|
}
|
||||||
} catch (e: MXCryptoError) {
|
} catch (e: MXCryptoError) {
|
||||||
Timber.v(e, "Failed to decrypt event $eventId")
|
Timber.w(e, "Failed to decrypt event $eventId")
|
||||||
if (e is MXCryptoError.Base && e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
if (e is MXCryptoError.Base && e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
||||||
// Keep track of unknown sessions to automatically try to decrypt on new session
|
// Keep track of unknown sessions to automatically try to decrypt on new session
|
||||||
realm.executeTransaction {
|
realm.executeTransaction {
|
||||||
|
@ -76,12 +76,12 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu
|
|||||||
val timelineEvent = summary.timelineEvent?.firstOrNull() ?: continue
|
val timelineEvent = summary.timelineEvent?.firstOrNull() ?: continue
|
||||||
val isLoaded = nonFilteredEvents.where()
|
val isLoaded = nonFilteredEvents.where()
|
||||||
.equalTo(TimelineEventEntityFields.EVENT_ID, timelineEvent.eventId).findFirst() != null
|
.equalTo(TimelineEventEntityFields.EVENT_ID, timelineEvent.eventId).findFirst() != null
|
||||||
val displayIndex = timelineEvent.root?.displayIndex
|
val displayIndex = timelineEvent.displayIndex
|
||||||
|
|
||||||
if (isLoaded && displayIndex != null) {
|
if (isLoaded) {
|
||||||
// Then we are looking for the first displayable event after the hidden one
|
// Then we are looking for the first displayable event after the hidden one
|
||||||
val firstDisplayedEvent = filteredEvents.where()
|
val firstDisplayedEvent = filteredEvents.where()
|
||||||
.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex)
|
.lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, displayIndex)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
|
|
||||||
// If we find one, we should
|
// If we find one, we should
|
||||||
|
@ -17,23 +17,37 @@
|
|||||||
package im.vector.matrix.android.internal.session.room.timeline
|
package im.vector.matrix.android.internal.session.room.timeline
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.internal.database.helper.*
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomMemberContent
|
||||||
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
|
import im.vector.matrix.android.internal.database.helper.addOrUpdate
|
||||||
|
import im.vector.matrix.android.internal.database.helper.addStateEvent
|
||||||
|
import im.vector.matrix.android.internal.database.helper.addTimelineEvent
|
||||||
|
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
|
||||||
|
import im.vector.matrix.android.internal.database.helper.merge
|
||||||
|
import im.vector.matrix.android.internal.database.mapper.toEntity
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
import im.vector.matrix.android.internal.database.query.create
|
import im.vector.matrix.android.internal.database.query.create
|
||||||
import im.vector.matrix.android.internal.database.query.find
|
import im.vector.matrix.android.internal.database.query.find
|
||||||
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
|
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
|
||||||
|
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||||
|
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||||
|
import im.vector.matrix.android.internal.database.query.latestEvent
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||||
import io.realm.kotlin.createObject
|
import io.realm.Realm
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert Chunk in DB, and eventually merge with existing chunk event
|
* Insert Chunk in DB, and eventually merge with existing chunk event
|
||||||
*/
|
*/
|
||||||
internal class TokenChunkEventPersistor @Inject constructor(private val monarchy: Monarchy,
|
internal class TokenChunkEventPersistor @Inject constructor(private val monarchy: Monarchy) {
|
||||||
private val timelineEventSenderVisitor: TimelineEventSenderVisitor) {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
* <pre>
|
||||||
@ -111,9 +125,6 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
|||||||
.awaitTransaction { realm ->
|
.awaitTransaction { realm ->
|
||||||
Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
|
Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
|
||||||
|
|
||||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
|
||||||
?: realm.createObject(roomId)
|
|
||||||
|
|
||||||
val nextToken: String?
|
val nextToken: String?
|
||||||
val prevToken: String?
|
val prevToken: String?
|
||||||
if (direction == PaginationDirection.FORWARDS) {
|
if (direction == PaginationDirection.FORWARDS) {
|
||||||
@ -124,50 +135,23 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
|||||||
prevToken = receivedChunk.end
|
prevToken = receivedChunk.end
|
||||||
}
|
}
|
||||||
|
|
||||||
val shouldSkip = ChunkEntity.find(realm, roomId, nextToken = nextToken) != null
|
|
||||||
|| ChunkEntity.find(realm, roomId, prevToken = prevToken) != null
|
|
||||||
|
|
||||||
val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken)
|
val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken)
|
||||||
val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken)
|
val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken)
|
||||||
|
|
||||||
// The current chunk is the one we will keep all along the merge processChanges.
|
// The current chunk is the one we will keep all along the merge processChanges.
|
||||||
// We try to look for a chunk next to the token,
|
// We try to look for a chunk next to the token,
|
||||||
// otherwise we create a whole new one which is unlinked (not live)
|
// otherwise we create a whole new one which is unlinked (not live)
|
||||||
|
val currentChunk = if (direction == PaginationDirection.FORWARDS) {
|
||||||
var currentChunk = if (direction == PaginationDirection.FORWARDS) {
|
|
||||||
prevChunk?.apply { this.nextToken = nextToken }
|
prevChunk?.apply { this.nextToken = nextToken }
|
||||||
} else {
|
} else {
|
||||||
nextChunk?.apply { this.prevToken = prevToken }
|
nextChunk?.apply { this.prevToken = prevToken }
|
||||||
}
|
}
|
||||||
?: ChunkEntity.create(realm, prevToken, nextToken, isUnlinked = true)
|
?: ChunkEntity.create(realm, prevToken, nextToken)
|
||||||
|
|
||||||
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
|
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
|
||||||
Timber.v("Reach end of $roomId")
|
handleReachEnd(realm, roomId, direction, currentChunk)
|
||||||
currentChunk.isLastBackward = true
|
|
||||||
} else if (!shouldSkip) {
|
|
||||||
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
|
|
||||||
val timelineEvents = receivedChunk.events.mapNotNull {
|
|
||||||
currentChunk.add(roomId, it, direction)
|
|
||||||
}
|
|
||||||
// Then we merge chunks if needed
|
|
||||||
if (currentChunk != prevChunk && prevChunk != null) {
|
|
||||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
|
|
||||||
} else if (currentChunk != nextChunk && nextChunk != null) {
|
|
||||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, nextChunk)
|
|
||||||
} else {
|
} else {
|
||||||
val newEventIds = receivedChunk.events.mapNotNull { it.eventId }
|
handlePagination(realm, roomId, direction, receivedChunk, currentChunk)
|
||||||
val overlappedChunks = ChunkEntity.findAllIncludingEvents(realm, newEventIds)
|
|
||||||
overlappedChunks
|
|
||||||
.filter { it != currentChunk }
|
|
||||||
.forEach { overlapped ->
|
|
||||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, overlapped)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
roomEntity.addOrUpdate(currentChunk)
|
|
||||||
for (stateEvent in receivedChunk.stateEvents) {
|
|
||||||
roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked)
|
|
||||||
}
|
|
||||||
timelineEventSenderVisitor.visit(timelineEvents)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return if (receivedChunk.events.isEmpty()) {
|
return if (receivedChunk.events.isEmpty()) {
|
||||||
@ -181,22 +165,90 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleMerge(roomEntity: RoomEntity,
|
private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) {
|
||||||
direction: PaginationDirection,
|
Timber.v("Reach end of $roomId")
|
||||||
currentChunk: ChunkEntity,
|
if (direction == PaginationDirection.FORWARDS) {
|
||||||
otherChunk: ChunkEntity): ChunkEntity {
|
val currentLiveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
||||||
// We always merge the bottom chunk into top chunk, so we are always merging backwards
|
if (currentChunk != currentLiveChunk) {
|
||||||
Timber.v("Merge ${currentChunk.prevToken} | ${currentChunk.nextToken} with ${otherChunk.prevToken} | ${otherChunk.nextToken}")
|
currentChunk.isLastForward = true
|
||||||
return if (direction == PaginationDirection.BACKWARDS && !otherChunk.isLastForward) {
|
currentLiveChunk?.deleteOnCascade()
|
||||||
val events = currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS)
|
RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
|
||||||
timelineEventSenderVisitor.visit(events)
|
latestPreviewableEvent = TimelineEventEntity.latestEvent(
|
||||||
roomEntity.deleteOnCascade(otherChunk)
|
realm,
|
||||||
currentChunk
|
roomId,
|
||||||
} else {
|
includesSending = true,
|
||||||
val events = otherChunk.merge(roomEntity.roomId, currentChunk, PaginationDirection.BACKWARDS)
|
filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES
|
||||||
timelineEventSenderVisitor.visit(events)
|
)
|
||||||
roomEntity.deleteOnCascade(currentChunk)
|
|
||||||
otherChunk
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
currentChunk.isLastBackward = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePagination(
|
||||||
|
realm: Realm,
|
||||||
|
roomId: String,
|
||||||
|
direction: PaginationDirection,
|
||||||
|
receivedChunk: TokenChunkEvent,
|
||||||
|
currentChunk: ChunkEntity
|
||||||
|
) {
|
||||||
|
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
|
||||||
|
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
|
||||||
|
val eventList = receivedChunk.events
|
||||||
|
val stateEvents = receivedChunk.stateEvents
|
||||||
|
|
||||||
|
for (stateEvent in stateEvents) {
|
||||||
|
val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED).let {
|
||||||
|
realm.copyToRealmOrUpdate(it)
|
||||||
|
}
|
||||||
|
currentChunk.addStateEvent(roomId, stateEventEntity, direction)
|
||||||
|
if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null) {
|
||||||
|
roomMemberContentsByUser[stateEvent.stateKey] = stateEvent.content.toModel<RoomMemberContent>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val eventIds = ArrayList<String>(eventList.size)
|
||||||
|
for (event in eventList) {
|
||||||
|
if (event.eventId == null || event.senderId == null) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
eventIds.add(event.eventId)
|
||||||
|
val eventEntity = event.toEntity(roomId, SendState.SYNCED).let {
|
||||||
|
realm.copyToRealmOrUpdate(it)
|
||||||
|
}
|
||||||
|
if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) {
|
||||||
|
val contentToUse = if (direction == PaginationDirection.BACKWARDS) {
|
||||||
|
event.prevContent
|
||||||
|
} else {
|
||||||
|
event.content
|
||||||
|
}
|
||||||
|
roomMemberContentsByUser[event.stateKey] = contentToUse.toModel<RoomMemberContent>()
|
||||||
|
}
|
||||||
|
|
||||||
|
currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
|
||||||
|
}
|
||||||
|
val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds)
|
||||||
|
val chunksToDelete = ArrayList<ChunkEntity>()
|
||||||
|
chunks.forEach {
|
||||||
|
if (it != currentChunk) {
|
||||||
|
currentChunk.merge(roomId, it, direction)
|
||||||
|
chunksToDelete.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val shouldUpdateSummary = chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS
|
||||||
|
chunksToDelete.forEach {
|
||||||
|
it.deleteOnCascade()
|
||||||
|
}
|
||||||
|
if (shouldUpdateSummary) {
|
||||||
|
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
|
||||||
|
val latestPreviewableEvent = TimelineEventEntity.latestEvent(
|
||||||
|
realm,
|
||||||
|
roomId,
|
||||||
|
includesSending = true,
|
||||||
|
filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES
|
||||||
|
)
|
||||||
|
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
|
||||||
|
}
|
||||||
|
RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ import im.vector.matrix.android.internal.database.awaitTransaction
|
|||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.query.types
|
import im.vector.matrix.android.internal.database.query.whereTypes
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||||
import io.realm.OrderedCollectionChangeSet
|
import io.realm.OrderedCollectionChangeSet
|
||||||
@ -41,7 +41,7 @@ internal class RoomTombstoneEventLiveObserver @Inject constructor(@SessionDataba
|
|||||||
: RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
: RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
||||||
|
|
||||||
override val query = Monarchy.Query<EventEntity> {
|
override val query = Monarchy.Query<EventEntity> {
|
||||||
EventEntity.types(it, listOf(EventType.STATE_ROOM_TOMBSTONE))
|
EventEntity.whereTypes(it, listOf(EventType.STATE_ROOM_TOMBSTONE))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||||
|
@ -41,7 +41,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
|
|||||||
// Decrypt event if necessary
|
// Decrypt event if necessary
|
||||||
decryptEvent(event, null)
|
decryptEvent(event, null)
|
||||||
if (event.getClearType() == EventType.MESSAGE
|
if (event.getClearType() == EventType.MESSAGE
|
||||||
&& event.getClearContent()?.toModel<MessageContent>()?.type == "m.bad.encrypted") {
|
&& event.getClearContent()?.toModel<MessageContent>()?.msgType == "m.bad.encrypted") {
|
||||||
Timber.e("## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}")
|
Timber.e("## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}")
|
||||||
} else {
|
} else {
|
||||||
verificationService.onToDeviceEvent(event)
|
verificationService.onToDeviceEvent(event)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user