Merge pull request #792 from vector-im/feature/stabilization

Feature/stabilization
This commit is contained in:
Benoit Marty 2020-01-09 11:43:52 +01:00 committed by GitHub
commit 898bf234da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
103 changed files with 1447 additions and 1091 deletions

View file

@ -17,13 +17,16 @@
package im.vector.matrix.rx package im.vector.matrix.rx
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
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.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.notification.RoomNotificationState import im.vector.matrix.android.api.session.room.notification.RoomNotificationState
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.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
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 io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
@ -31,18 +34,22 @@ class RxRoom(private val room: Room) {
fun liveRoomSummary(): Observable<Optional<RoomSummary>> { fun liveRoomSummary(): Observable<Optional<RoomSummary>> {
return room.getRoomSummaryLive().asObservable() return room.getRoomSummaryLive().asObservable()
.startWith(room.roomSummary().toOptional())
} }
fun liveRoomMemberIds(): Observable<List<String>> { fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable<List<RoomMember>> {
return room.getRoomMemberIdsLive().asObservable() return room.getRoomMembersLive(queryParams).asObservable()
.startWith(room.getRoomMembers(queryParams))
} }
fun liveAnnotationSummary(eventId: String): Observable<Optional<EventAnnotationsSummary>> { fun liveAnnotationSummary(eventId: String): Observable<Optional<EventAnnotationsSummary>> {
return room.getEventSummaryLive(eventId).asObservable() return room.getEventAnnotationsSummaryLive(eventId).asObservable()
.startWith(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())
} }
fun liveReadMarker(): Observable<Optional<String>> { fun liveReadMarker(): Observable<Optional<String>> {

View file

@ -18,8 +18,10 @@ package im.vector.matrix.rx
import androidx.paging.PagedList import androidx.paging.PagedList
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.pushers.Pusher import im.vector.matrix.android.api.session.pushers.Pusher
import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.sync.SyncState
@ -30,40 +32,43 @@ import io.reactivex.Single
class RxSession(private val session: Session) { class RxSession(private val session: Session) {
fun liveRoomSummaries(): Observable<List<RoomSummary>> { fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
return session.liveRoomSummaries().asObservable() return session.getRoomSummariesLive(queryParams).asObservable()
.startWith(session.getRoomSummaries(queryParams))
} }
fun liveGroupSummaries(): Observable<List<GroupSummary>> { fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable<List<GroupSummary>> {
return session.liveGroupSummaries().asObservable() return session.getGroupSummariesLive(queryParams).asObservable()
.startWith(session.getGroupSummaries(queryParams))
} }
fun liveBreadcrumbs(): Observable<List<RoomSummary>> { fun liveBreadcrumbs(): Observable<List<RoomSummary>> {
return session.liveBreadcrumbs().asObservable() return session.getBreadcrumbsLive().asObservable()
.startWith(session.getBreadcrumbs())
} }
fun liveSyncState(): Observable<SyncState> { fun liveSyncState(): Observable<SyncState> {
return session.syncState().asObservable() return session.getSyncStateLive().asObservable()
} }
fun livePushers(): Observable<List<Pusher>> { fun livePushers(): Observable<List<Pusher>> {
return session.livePushers().asObservable() return session.getPushersLive().asObservable()
} }
fun liveUser(userId: String): Observable<Optional<User>> { fun liveUser(userId: String): Observable<Optional<User>> {
return session.liveUser(userId).asObservable().distinctUntilChanged() return session.getUserLive(userId).asObservable().distinctUntilChanged()
} }
fun liveUsers(): Observable<List<User>> { fun liveUsers(): Observable<List<User>> {
return session.liveUsers().asObservable() return session.getUsersLive().asObservable()
} }
fun liveIgnoredUsers(): Observable<List<User>> { fun liveIgnoredUsers(): Observable<List<User>> {
return session.liveIgnoredUsers().asObservable() return session.getIgnoredUsersLive().asObservable()
} }
fun livePagedUsers(filter: String? = null): Observable<PagedList<User>> { fun livePagedUsers(filter: String? = null): Observable<PagedList<User>> {
return session.livePagedUsers(filter).asObservable() return session.getPagedUsersLive(filter).asObservable()
} }
fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder { fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder {

View file

@ -10,7 +10,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath "io.realm:realm-gradle-plugin:5.12.0" classpath "io.realm:realm-gradle-plugin:6.0.2"
} }
} }
@ -102,7 +102,6 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "androidx.appcompat:appcompat:1.1.0" implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.recyclerview:recyclerview:1.1.0-beta05"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version" kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
@ -119,14 +118,14 @@ dependencies {
implementation "ru.noties.markwon:core:$markwon_version" implementation "ru.noties.markwon:core:$markwon_version"
// Image // Image
implementation 'androidx.exifinterface:exifinterface:1.0.0' implementation 'androidx.exifinterface:exifinterface:1.1.0'
// Database // Database
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1' implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
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-alpha01" implementation "androidx.work:work-runtime-ktx:2.3.0-beta02"
// FP // FP
implementation "io.arrow-kt:arrow-core:$arrow_version" implementation "io.arrow-kt:arrow-core:$arrow_version"

View file

@ -19,7 +19,10 @@ package im.vector.matrix.android.session.room.timeline
import androidx.test.ext.junit.runners.AndroidJUnit4 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.internal.database.helper.* import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.database.helper.add
import im.vector.matrix.android.internal.database.helper.lastStateIndex
import im.vector.matrix.android.internal.database.helper.merge
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
@ -29,7 +32,6 @@ import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeR
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
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.shouldBeTrue import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldEqual import org.amshove.kluent.shouldEqual
import org.junit.Before import org.junit.Before
@ -146,30 +148,6 @@ internal class ChunkEntityTest : InstrumentedTest {
} }
} }
@Test
fun merge_shouldEventsBeLinked_whenMergingLinkedWithUnlinked() {
monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject()
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = false)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.isUnlinked().shouldBeFalse()
}
}
@Test
fun merge_shouldEventsBeUnlinked_whenMergingUnlinkedWithUnlinked() {
monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject()
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.isUnlinked().shouldBeTrue()
}
}
@Test @Test
fun merge_shouldPrevTokenMerged_whenMergingForwards() { fun merge_shouldPrevTokenMerged_whenMergingForwards() {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
@ -177,8 +155,8 @@ 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, isUnlinked = true) chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.FORWARDS) chunk1.merge("roomId", chunk2, PaginationDirection.FORWARDS)
chunk1.prevToken shouldEqual prevToken chunk1.prevToken shouldEqual prevToken
} }
@ -191,10 +169,19 @@ 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, isUnlinked = true) chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.nextToken shouldEqual nextToken chunk1.nextToken shouldEqual nextToken
} }
} }
private fun ChunkEntity.addAll(roomId: String,
events: List<Event>,
direction: PaginationDirection,
stateIndexOffset: Int = 0) {
events.forEach { event ->
add(roomId, event, direction, stateIndexOffset)
}
}
} }

View file

@ -16,7 +16,6 @@
package im.vector.matrix.android.session.room.timeline package im.vector.matrix.android.session.room.timeline
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
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.EventType import im.vector.matrix.android.api.session.events.model.EventType
@ -25,12 +24,6 @@ import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomMember
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.internal.database.helper.addAll
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import io.realm.kotlin.createObject
import kotlin.random.Random import kotlin.random.Random
object RoomDataHelper { object RoomDataHelper {
@ -73,19 +66,4 @@ object RoomDataHelper {
val roomMember = RoomMember(Membership.JOIN, "Fake name #${Random.nextLong()}").toContent() val roomMember = RoomMember(Membership.JOIN, "Fake name #${Random.nextLong()}").toContent()
return createFakeEvent(EventType.STATE_ROOM_MEMBER, roomMember) return createFakeEvent(EventType.STATE_ROOM_MEMBER, roomMember)
} }
fun fakeInitialSync(monarchy: Monarchy, roomId: String) {
monarchy.runTransactionSync { realm ->
val roomEntity = realm.createObject<RoomEntity>(roomId)
roomEntity.membership = Membership.JOIN
val eventList = createFakeListOfEvents(10)
val chunkEntity = realm.createObject<ChunkEntity>().apply {
nextToken = null
prevToken = Random.nextLong(System.currentTimeMillis()).toString()
isLastForward = true
}
chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS)
roomEntity.addOrUpdate(chunkEntity)
}
}
} }

View file

@ -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.api.query
/**
* Basic query language. All these cases are mutually exclusive.
*/
sealed class QueryStringValue {
object NoCondition : QueryStringValue()
object IsNull : QueryStringValue()
object IsNotNull : QueryStringValue()
object IsEmpty : QueryStringValue()
object IsNotEmpty : QueryStringValue()
data class Equals(val string: String, val case: Case) : QueryStringValue()
data class Contains(val string: String, val case: Case) : QueryStringValue()
enum class Case {
SENSITIVE,
INSENSITIVE
}
}

View file

@ -107,7 +107,7 @@ interface Session :
* This method allows to listen the sync state. * This method allows to listen the sync state.
* @return a [LiveData] of [SyncState]. * @return a [LiveData] of [SyncState].
*/ */
fun syncState(): LiveData<SyncState> fun getSyncStateLive(): LiveData<SyncState>
/** /**
* This methods return true if an initial sync has been processed * This methods return true if an initial sync has been processed

View file

@ -38,9 +38,15 @@ interface GroupService {
*/ */
fun getGroupSummary(groupId: String): GroupSummary? fun getGroupSummary(groupId: String): GroupSummary?
/**
* Get a list of group summaries. This list is a snapshot of the data.
* @return the list of [GroupSummary]
*/
fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List<GroupSummary>
/** /**
* Get a live list of group summaries. This list is refreshed as soon as the data changes. * Get a live list of group summaries. This list is refreshed as soon as the data changes.
* @return the [LiveData] of [GroupSummary] * @return the [LiveData] of [GroupSummary]
*/ */
fun liveGroupSummaries(): LiveData<List<GroupSummary>> fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData<List<GroupSummary>>
} }

View file

@ -0,0 +1,44 @@
/*
* 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.group
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.room.model.Membership
fun groupSummaryQueryParams(init: (GroupSummaryQueryParams.Builder.() -> Unit) = {}): GroupSummaryQueryParams {
return GroupSummaryQueryParams.Builder().apply(init).build()
}
/**
* This class can be used to filter group summaries
*/
data class GroupSummaryQueryParams(
val displayName: QueryStringValue,
val memberships: List<Membership>
) {
class Builder {
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
var memberships: List<Membership> = Membership.all()
fun build() = GroupSummaryQueryParams(
displayName = displayName,
memberships = memberships
)
}
}

View file

@ -58,7 +58,7 @@ interface PushersService {
const val EVENT_ID_ONLY = "event_id_only" const val EVENT_ID_ONLY = "event_id_only"
} }
fun livePushers(): LiveData<List<Pusher>> fun getPushersLive(): LiveData<List<Pusher>>
fun pushers() : List<Pusher> fun pushers() : List<Pusher>
} }

View file

@ -56,5 +56,8 @@ interface Room :
*/ */
fun getRoomSummaryLive(): LiveData<Optional<RoomSummary>> fun getRoomSummaryLive(): LiveData<Optional<RoomSummary>>
/**
* A current snapshot of [RoomSummary] associated with the room
*/
fun roomSummary(): RoomSummary? fun roomSummary(): RoomSummary?
} }

View file

@ -60,16 +60,28 @@ interface RoomService {
fun getRoomSummary(roomIdOrAlias: String): RoomSummary? fun getRoomSummary(roomIdOrAlias: String): RoomSummary?
/** /**
* Get a live list of room summaries. This list is refreshed as soon as the data changes. * Get a snapshot list of room summaries.
* @return the [LiveData] of [RoomSummary] * @return the immutable list of [RoomSummary]
*/ */
fun liveRoomSummaries(): LiveData<List<RoomSummary>> fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List<RoomSummary>
/**
* Get a live list of room summaries. This list is refreshed as soon as the data changes.
* @return the [LiveData] of List[RoomSummary]
*/
fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>>
/**
* Get a snapshot list of Breadcrumbs
* @return the immutable list of [RoomSummary]
*/
fun getBreadcrumbs(): List<RoomSummary>
/** /**
* Get a live list of Breadcrumbs * Get a live list of Breadcrumbs
* @return the [LiveData] of [RoomSummary] * @return the [LiveData] of [RoomSummary]
*/ */
fun liveBreadcrumbs(): LiveData<List<RoomSummary>> fun getBreadcrumbsLive(): LiveData<List<RoomSummary>>
/** /**
* Inform the Matrix SDK that a room is displayed. * Inform the Matrix SDK that a room is displayed.

View file

@ -0,0 +1,48 @@
/*
* 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
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.room.model.Membership
fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams {
return RoomSummaryQueryParams.Builder().apply(init).build()
}
/**
* This class can be used to filter room summaries to use with:
* [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService]
*/
data class RoomSummaryQueryParams(
val displayName: QueryStringValue,
val canonicalAlias: QueryStringValue,
val memberships: List<Membership>
) {
class Builder {
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
var memberships: List<Membership> = Membership.all()
fun build() = RoomSummaryQueryParams(
displayName = displayName,
canonicalAlias = canonicalAlias,
memberships = memberships
)
}
}

View file

@ -41,11 +41,18 @@ interface MembershipService {
fun getRoomMember(userId: String): RoomMember? fun getRoomMember(userId: String): RoomMember?
/** /**
* Return all the roomMembers ids of the room * Return all the roomMembers of the room with params
* * @param queryParams the params to query for
* @return a roomMember list.
*/
fun getRoomMembers(queryParams: RoomMemberQueryParams): List<RoomMember>
/**
* Return all the roomMembers of the room filtered by memberships
* @param queryParams the params to query for
* @return a [LiveData] of roomMember list. * @return a [LiveData] of roomMember list.
*/ */
fun getRoomMemberIdsLive(): LiveData<List<String>> fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData<List<RoomMember>>
fun getNumberOfJoinedMembers(): Int fun getNumberOfJoinedMembers(): Int

View file

@ -0,0 +1,44 @@
/*
* 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.members
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.room.model.Membership
fun roomMemberQueryParams(init: (RoomMemberQueryParams.Builder.() -> Unit) = {}): RoomMemberQueryParams {
return RoomMemberQueryParams.Builder().apply(init).build()
}
/**
* This class can be used to filter room members
*/
data class RoomMemberQueryParams(
val displayName: QueryStringValue,
val memberships: List<Membership>
) {
class Builder {
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
var memberships: List<Membership> = Membership.all()
fun build() = RoomMemberQueryParams(
displayName = displayName,
memberships = memberships
)
}
}

View file

@ -43,4 +43,14 @@ enum class Membership(val value: String) {
fun isLeft(): Boolean { fun isLeft(): Boolean {
return this == KNOCK || this == LEAVE || this == BAN return this == KNOCK || this == LEAVE || this == BAN
} }
companion object {
fun activeMemberships(): List<Membership> {
return listOf(INVITE, JOIN)
}
fun all(): List<Membership> {
return values().asList()
}
}
} }

View file

@ -16,23 +16,12 @@
package im.vector.matrix.android.api.session.room.model package im.vector.matrix.android.api.session.room.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.UnsignedData
/** /**
* Class representing the EventType.STATE_ROOM_MEMBER state event content * Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content
*/ */
@JsonClass(generateAdapter = true)
data class RoomMember( data class RoomMember(
@Json(name = "membership") val membership: Membership, val membership: Membership,
@Json(name = "reason") val reason: String? = null, val userId: String,
@Json(name = "displayname") val displayName: String? = null, val displayName: String? = null,
@Json(name = "avatar_url") val avatarUrl: String? = null, val avatarUrl: String? = null
@Json(name = "is_direct") val isDirect: Boolean = false, )
@Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null,
@Json(name = "unsigned") val unsignedData: UnsignedData? = null
) {
val safeReason
get() = reason?.takeIf { it.isNotBlank() }
}

View file

@ -0,0 +1,38 @@
/*
* 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.api.session.room.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.UnsignedData
/**
* Class representing the EventType.STATE_ROOM_MEMBER state event content
*/
@JsonClass(generateAdapter = true)
data class RoomMemberContent(
@Json(name = "membership") val membership: Membership,
@Json(name = "reason") val reason: String? = null,
@Json(name = "displayname") val displayName: String? = null,
@Json(name = "avatar_url") val avatarUrl: String? = null,
@Json(name = "is_direct") val isDirect: Boolean = false,
@Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null,
@Json(name = "unsigned") val unsignedData: UnsignedData? = null
) {
val safeReason
get() = reason?.takeIf { it.isNotBlank() }
}

View file

@ -41,7 +41,8 @@ data class RoomSummary(
val membership: Membership = Membership.NONE, val membership: Membership = Membership.NONE,
val versioningState: VersioningState = VersioningState.NONE, val versioningState: VersioningState = VersioningState.NONE,
val readMarkerId: String? = null, val readMarkerId: String? = null,
val userDrafts: List<UserDraft> = emptyList() val userDrafts: List<UserDraft> = emptyList(),
var isEncrypted: Boolean
) { ) {
val isVersioned: Boolean val isVersioned: Boolean

View file

@ -108,5 +108,17 @@ interface RelationService {
replyText: CharSequence, replyText: CharSequence,
autoMarkdown: Boolean = false): Cancelable? autoMarkdown: Boolean = false): Cancelable?
fun getEventSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>> /**
* Get the current EventAnnotationsSummary
* @param eventId the eventId to look for EventAnnotationsSummary
* @return the EventAnnotationsSummary found
*/
fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary?
/**
* Get a LiveData of EventAnnotationsSummary for the specified eventId
* @param eventId the eventId to look for EventAnnotationsSummary
* @return the LiveData of EventAnnotationsSummary
*/
fun getEventAnnotationsSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>>
} }

View file

@ -50,25 +50,25 @@ interface UserService {
* @param userId the userId to look for. * @param userId the userId to look for.
* @return a LiveData of user with userId * @return a LiveData of user with userId
*/ */
fun liveUser(userId: String): LiveData<Optional<User>> fun getUserLive(userId: String): LiveData<Optional<User>>
/** /**
* Observe a live list of users sorted alphabetically * Observe a live list of users sorted alphabetically
* @return a Livedata of users * @return a Livedata of users
*/ */
fun liveUsers(): LiveData<List<User>> fun getUsersLive(): LiveData<List<User>>
/** /**
* Observe a live [PagedList] of users sorted alphabetically. You can filter the users. * Observe a live [PagedList] of users sorted alphabetically. You can filter the users.
* @param filter the filter. It will look into userId and displayName. * @param filter the filter. It will look into userId and displayName.
* @return a Livedata of users * @return a Livedata of users
*/ */
fun livePagedUsers(filter: String? = null): LiveData<PagedList<User>> fun getPagedUsersLive(filter: String? = null): LiveData<PagedList<User>>
/** /**
* Get list of ignored users * Get list of ignored users
*/ */
fun liveIgnoredUsers(): LiveData<List<User>> fun getIgnoredUsersLive(): LiveData<List<User>>
/** /**
* Ignore users * Ignore users

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.api.util
import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
@ -146,3 +147,4 @@ fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, ava
fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl) fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl)
fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl) fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl)
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name, avatarUrl) fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name, avatarUrl)
fun RoomMember.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)

View file

@ -59,6 +59,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val
.apply() .apply()
val realmConfiguration = RealmConfiguration.Builder() val realmConfiguration = RealmConfiguration.Builder()
.compactOnLaunch()
.directory(directory) .directory(directory)
.name(REALM_NAME) .name(REALM_NAME)
.apply { .apply {

View file

@ -28,15 +28,7 @@ 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.Sort import io.realm.Sort
import io.realm.kotlin.createObject
// By default if a chunk is empty we consider it unlinked
internal fun ChunkEntity.isUnlinked(): Boolean {
assertIsManaged()
return timelineEvents.where()
.equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, false)
.findAll()
.isEmpty()
}
internal fun ChunkEntity.deleteOnCascade() { internal fun ChunkEntity.deleteOnCascade() {
assertIsManaged() assertIsManaged()
@ -46,11 +38,10 @@ internal fun ChunkEntity.deleteOnCascade() {
internal fun ChunkEntity.merge(roomId: String, internal fun ChunkEntity.merge(roomId: String,
chunkToMerge: ChunkEntity, chunkToMerge: ChunkEntity,
direction: PaginationDirection) { direction: PaginationDirection): List<TimelineEventEntity> {
assertIsManaged() assertIsManaged()
val isChunkToMergeUnlinked = chunkToMerge.isUnlinked() val isChunkToMergeUnlinked = chunkToMerge.isUnlinked
val isCurrentChunkUnlinked = this.isUnlinked() val isCurrentChunkUnlinked = isUnlinked
val isUnlinked = isCurrentChunkUnlinked && isChunkToMergeUnlinked
if (isCurrentChunkUnlinked && !isChunkToMergeUnlinked) { if (isCurrentChunkUnlinked && !isChunkToMergeUnlinked) {
this.timelineEvents.forEach { it.root?.isUnlinked = false } this.timelineEvents.forEach { it.root?.isUnlinked = false }
@ -65,49 +56,21 @@ internal fun ChunkEntity.merge(roomId: String,
this.isLastBackward = chunkToMerge.isLastBackward this.isLastBackward = chunkToMerge.isLastBackward
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING) eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
} }
val events = eventsToMerge.mapNotNull { it.root?.asDomain() } return eventsToMerge
val eventIds = ArrayList<String>() .mapNotNull {
events.forEach { event -> val event = it.root?.asDomain() ?: return@mapNotNull null
add(roomId, event, direction, isUnlinked = isUnlinked) add(roomId, event, direction)
if (event.eventId != null) { }
eventIds.add(event.eventId)
}
}
updateSenderDataFor(eventIds)
}
internal fun ChunkEntity.addAll(roomId: String,
events: List<Event>,
direction: PaginationDirection,
stateIndexOffset: Int = 0,
// Set to true for Event retrieved from a Permalink (i.e. not linked to live Chunk)
isUnlinked: Boolean = false) {
assertIsManaged()
val eventIds = ArrayList<String>()
events.forEach { event ->
add(roomId, event, direction, stateIndexOffset, isUnlinked)
if (event.eventId != null) {
eventIds.add(event.eventId)
}
}
updateSenderDataFor(eventIds)
}
internal fun ChunkEntity.updateSenderDataFor(eventIds: List<String>) {
for (eventId in eventIds) {
val timelineEventEntity = timelineEvents.find(eventId) ?: continue
timelineEventEntity.updateSenderData()
}
} }
internal fun ChunkEntity.add(roomId: String, internal fun ChunkEntity.add(roomId: String,
event: Event, event: Event,
direction: PaginationDirection, direction: PaginationDirection,
stateIndexOffset: Int = 0, stateIndexOffset: Int = 0
isUnlinked: Boolean = false) { ): TimelineEventEntity? {
assertIsManaged() assertIsManaged()
if (event.eventId != null && timelineEvents.find(event.eventId) != null) { if (event.eventId != null && timelineEvents.find(event.eventId) != null) {
return return null
} }
var currentDisplayIndex = lastDisplayIndex(direction, 0) var currentDisplayIndex = lastDisplayIndex(direction, 0)
if (direction == PaginationDirection.FORWARDS) { if (direction == PaginationDirection.FORWARDS) {
@ -129,12 +92,15 @@ internal fun ChunkEntity.add(roomId: String,
} }
} }
val isChunkUnlinked = isUnlinked
val localId = TimelineEventEntity.nextId(realm) val localId = TimelineEventEntity.nextId(realm)
val eventId = event.eventId ?: "" val eventId = event.eventId ?: ""
val senderId = event.senderId ?: "" val senderId = event.senderId ?: ""
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst() val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
?: ReadReceiptsSummaryEntity(eventId, roomId) ?: 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
@ -151,13 +117,15 @@ internal fun ChunkEntity.add(roomId: String,
} }
} }
val eventEntity = TimelineEventEntity(localId).also { val rootEvent = event.toEntity(roomId).apply {
it.root = event.toEntity(roomId).apply { this.stateIndex = currentStateIndex
this.stateIndex = currentStateIndex this.displayIndex = currentDisplayIndex
this.isUnlinked = isUnlinked this.sendState = SendState.SYNCED
this.displayIndex = currentDisplayIndex this.isUnlinked = isChunkUnlinked
this.sendState = SendState.SYNCED }
} val eventEntity = realm.createObject<TimelineEventEntity>().also {
it.localId = localId
it.root = realm.copyToRealm(rootEvent)
it.eventId = eventId it.eventId = eventId
it.roomId = roomId it.roomId = roomId
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
@ -165,6 +133,7 @@ internal fun ChunkEntity.add(roomId: String,
} }
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
timelineEvents.add(position, eventEntity) timelineEvents.add(position, eventEntity)
return eventEntity
} }
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {

View file

@ -60,7 +60,7 @@ internal fun RoomEntity.addSendingEvent(event: Event) {
this.sendState = SendState.UNSENT this.sendState = SendState.UNSENT
} }
val roomMembers = RoomMembers(realm, roomId) val roomMembers = RoomMembers(realm, roomId)
val myUser = roomMembers.get(senderId) val myUser = roomMembers.getLastRoomMember(senderId)
val localId = TimelineEventEntity.nextId(realm) val localId = TimelineEventEntity.nextId(realm)
val timelineEventEntity = TimelineEventEntity(localId).also { val timelineEventEntity = TimelineEventEntity(localId).also {
it.root = eventEntity it.root = eventEntity
@ -69,7 +69,6 @@ internal fun RoomEntity.addSendingEvent(event: Event) {
it.senderName = myUser?.displayName it.senderName = myUser?.displayName
it.senderAvatar = myUser?.avatarUrl it.senderAvatar = myUser?.avatarUrl
it.isUniqueDisplayName = roomMembers.isUniqueDisplayName(myUser?.displayName) it.isUniqueDisplayName = roomMembers.isUniqueDisplayName(myUser?.displayName)
it.senderMembershipEvent = roomMembers.queryRoomMemberEvent(senderId).findFirst()
} }
sendingTimelineEvents.add(0, timelineEventEntity) sendingTimelineEvents.add(0, timelineEventEntity)
} }

View file

@ -16,74 +16,9 @@
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.EventType import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.api.session.room.model.RoomMember
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.room.membership.RoomMembers
import io.realm.Realm import io.realm.Realm
import io.realm.RealmList
import io.realm.RealmQuery
internal fun TimelineEventEntity.updateSenderData() {
assertIsManaged()
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return
val stateIndex = root?.stateIndex ?: return
val senderId = root?.sender ?: return
val chunkEntity = chunk?.firstOrNull() ?: return
val isUnlinked = chunkEntity.isUnlinked()
var senderMembershipEvent: EventEntity?
var senderRoomMemberContent: String?
var senderRoomMemberPrevContent: String?
when {
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<RoomMember>()?.also {
this.senderAvatar = it.avatarUrl
this.senderName = it.displayName
this.isUniqueDisplayName = RoomMembers(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<RoomMember>()?.also {
if (this.senderAvatar == null && it.avatarUrl != null) {
this.senderAvatar = it.avatarUrl
}
if (this.senderName == null && it.displayName != null) {
this.senderName = it.displayName
this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName)
}
}
}
this.senderMembershipEvent = senderMembershipEvent
}
internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long { internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long {
val currentIdNum = realm.where(TimelineEventEntity::class.java).max(TimelineEventEntityFields.LOCAL_ID) val currentIdNum = realm.where(TimelineEventEntity::class.java).max(TimelineEventEntityFields.LOCAL_ID)
@ -93,10 +28,3 @@ internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long {
currentIdNum.toLong() + 1 currentIdNum.toLong() + 1
} }
} }
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)
}

View file

@ -0,0 +1,148 @@
/*
* 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.RoomMembers
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 = RoomMembers(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 = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName)
}
}
}
result.senderMembershipEventId = senderMembershipEvent?.eventId
return result
}
}

View file

@ -0,0 +1,36 @@
/*
* 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.mapper
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.internal.database.model.RoomMemberEntity
internal object RoomMemberMapper {
fun map(roomMemberEntity: RoomMemberEntity): RoomMember {
return RoomMember(
userId = roomMemberEntity.userId,
avatarUrl = roomMemberEntity.avatarUrl,
displayName = roomMemberEntity.displayName,
membership = roomMemberEntity.membership
)
}
}
internal fun RoomMemberEntity.asDomain(): RoomMember {
return RoomMemberMapper.map(this)
}

View file

@ -70,7 +70,8 @@ internal class RoomSummaryMapper @Inject constructor(
readMarkerId = roomSummaryEntity.readMarkerId, readMarkerId = roomSummaryEntity.readMarkerId,
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(), userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(),
canonicalAlias = roomSummaryEntity.canonicalAlias, canonicalAlias = roomSummaryEntity.canonicalAlias,
aliases = roomSummaryEntity.aliases.toList() aliases = roomSummaryEntity.aliases.toList(),
isEncrypted = roomSummaryEntity.isEncrypted
) )
} }
} }

View file

@ -30,7 +30,8 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
var backwardsDisplayIndex: Int? = null, var backwardsDisplayIndex: Int? = null,
var forwardsDisplayIndex: Int? = null, var forwardsDisplayIndex: Int? = null,
var backwardsStateIndex: Int? = null, var backwardsStateIndex: Int? = null,
var forwardsStateIndex: Int? = null var forwardsStateIndex: Int? = null,
var isUnlinked: Boolean = false
) : RealmObject() { ) : RealmObject() {
fun identifier() = "${prevToken}_$nextToken" fun identifier() = "${prevToken}_$nextToken"

View file

@ -0,0 +1,43 @@
/*
* 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.model
import im.vector.matrix.android.api.session.room.model.Membership
import io.realm.RealmObject
import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey
internal open class RoomMemberEntity(@PrimaryKey var primaryKey: String = "",
@Index var userId: String = "",
@Index var roomId: String = "",
var displayName: String = "",
var avatarUrl: String = "",
var reason: String? = null,
var isDirect: Boolean = false
) : RealmObject() {
private var membershipStr: String = Membership.NONE.name
var membership: Membership
get() {
return Membership.valueOf(membershipStr)
}
set(value) {
membershipStr = value.name
}
companion object
}

View file

@ -42,7 +42,9 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
var canonicalAlias: String? = null, var canonicalAlias: String? = null,
var aliases: RealmList<String> = RealmList(), var aliases: RealmList<String> = RealmList(),
var flatAliases: String = "" // this is required for querying
var flatAliases: String = "",
var isEncrypted: Boolean = false
) : RealmObject() { ) : RealmObject() {
private var membershipStr: String = Membership.NONE.name private var membershipStr: String = Membership.NONE.name

View file

@ -49,6 +49,7 @@ import io.realm.annotations.RealmModule
ReadMarkerEntity::class, ReadMarkerEntity::class,
UserDraftsEntity::class, UserDraftsEntity::class,
DraftEntity::class, DraftEntity::class,
HomeServerCapabilitiesEntity::class HomeServerCapabilitiesEntity::class,
RoomMemberEntity::class
]) ])
internal class SessionRealmModule internal class SessionRealmModule

View file

@ -29,7 +29,7 @@ internal open class TimelineEventEntity(var localId: Long = 0,
var senderName: String? = null, var senderName: String? = null,
var isUniqueDisplayName: Boolean = false, var isUniqueDisplayName: Boolean = false,
var senderAvatar: String? = null, var senderAvatar: String? = null,
var senderMembershipEvent: EventEntity? = null, var senderMembershipEventId: String? = null,
var readReceipts: ReadReceiptsSummaryEntity? = null var readReceipts: ReadReceiptsSummaryEntity? = null
) : RealmObject() { ) : RealmObject() {

View file

@ -57,9 +57,15 @@ internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: Str
return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull() return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull()
} }
internal fun ChunkEntity.Companion.create(realm: Realm, prevToken: String?, nextToken: String?): ChunkEntity { internal fun ChunkEntity.Companion.create(
realm: Realm,
prevToken: String?,
nextToken: String?,
isUnlinked: Boolean
): 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
} }
} }

View file

@ -0,0 +1,34 @@
/*
* 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.query
import im.vector.matrix.android.internal.database.model.RoomMemberEntity
import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
internal fun RoomMemberEntity.Companion.where(realm: Realm, roomId: String, userId: String? = null): RealmQuery<RoomMemberEntity> {
val query = realm
.where<RoomMemberEntity>()
.equalTo(RoomMemberEntityFields.ROOM_ID, roomId)
if (userId != null) {
query.equalTo(RoomMemberEntityFields.USER_ID, userId)
}
return query
}

View file

@ -54,7 +54,7 @@ internal fun TimelineEventEntity.Companion.where(realm: Realm,
internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List<TimelineEventEntity> { internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: Realm, senderMembershipEventId: String): List<TimelineEventEntity> {
return realm.where<TimelineEventEntity>() return realm.where<TimelineEventEntity>()
.equalTo(TimelineEventEntityFields.SENDER_MEMBERSHIP_EVENT.EVENT_ID, senderMembershipEventId) .equalTo(TimelineEventEntityFields.SENDER_MEMBERSHIP_EVENT_ID, senderMembershipEventId)
.findAll() .findAll()
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright 2020 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,12 +14,20 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.home.room.detail.composer package im.vector.matrix.android.internal.query
import im.vector.riotx.core.platform.VectorViewModelAction import io.realm.RealmObject
import io.realm.RealmQuery
sealed class TextComposerAction : VectorViewModelAction { fun <T : RealmObject, E : Enum<E>> RealmQuery<T>.process(field: String, enums: List<Enum<E>>): RealmQuery<T> {
data class QueryUsers(val query: CharSequence?) : TextComposerAction() val lastEnumValue = enums.lastOrNull()
data class QueryRooms(val query: CharSequence?) : TextComposerAction() beginGroup()
data class QueryGroups(val query: CharSequence?) : TextComposerAction() for (enumValue in enums) {
equalTo(field, enumValue.name)
if (enumValue != lastEnumValue) {
or()
}
}
endGroup()
return this
} }

View file

@ -0,0 +1,43 @@
/*
* 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.query
import im.vector.matrix.android.api.query.QueryStringValue
import io.realm.Case
import io.realm.RealmObject
import io.realm.RealmQuery
import timber.log.Timber
fun <T : RealmObject> RealmQuery<T>.process(field: String, queryStringValue: QueryStringValue): RealmQuery<T> {
when (queryStringValue) {
is QueryStringValue.NoCondition -> Timber.v("No condition to process")
is QueryStringValue.IsNotNull -> isNotNull(field)
is QueryStringValue.IsNull -> isNull(field)
is QueryStringValue.IsEmpty -> isEmpty(field)
is QueryStringValue.IsNotEmpty -> isNotEmpty(field)
is QueryStringValue.Equals -> equalTo(field, queryStringValue.string, queryStringValue.case.toRealmCase())
is QueryStringValue.Contains -> contains(field, queryStringValue.string, queryStringValue.case.toRealmCase())
}
return this
}
private fun QueryStringValue.Case.toRealmCase(): Case {
return when (this) {
QueryStringValue.Case.INSENSITIVE -> Case.INSENSITIVE
QueryStringValue.Case.SENSITIVE -> Case.SENSITIVE
}
}

View file

@ -156,7 +156,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
syncTaskSequencer.close() syncTaskSequencer.close()
} }
override fun syncState(): LiveData<SyncState> { override fun getSyncStateLive(): LiveData<SyncState> {
return getSyncThread().liveState() return getSyncThread().liveState()
} }

View file

@ -20,12 +20,16 @@ import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.group.Group import im.vector.matrix.android.api.session.group.Group
import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
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.GroupSummaryEntity import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields import im.vector.matrix.android.internal.database.model.GroupSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.query.process
import im.vector.matrix.android.internal.util.fetchCopyMap import im.vector.matrix.android.internal.util.fetchCopyMap
import io.realm.Realm
import io.realm.RealmQuery
import javax.inject.Inject import javax.inject.Inject
internal class DefaultGroupService @Inject constructor(private val monarchy: Monarchy) : GroupService { internal class DefaultGroupService @Inject constructor(private val monarchy: Monarchy) : GroupService {
@ -41,10 +45,23 @@ internal class DefaultGroupService @Inject constructor(private val monarchy: Mon
) )
} }
override fun liveGroupSummaries(): LiveData<List<GroupSummary>> { override fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List<GroupSummary> {
return monarchy.findAllMappedWithChanges( return monarchy.fetchAllMappedSync(
{ realm -> GroupSummaryEntity.where(realm).isNotEmpty(GroupSummaryEntityFields.DISPLAY_NAME) }, { groupSummariesQuery(it, groupSummaryQueryParams) },
{ it.asDomain() } { it.asDomain() }
) )
} }
override fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData<List<GroupSummary>> {
return monarchy.findAllMappedWithChanges(
{ groupSummariesQuery(it, groupSummaryQueryParams) },
{ it.asDomain() }
)
}
private fun groupSummariesQuery(realm: Realm, queryParams: GroupSummaryQueryParams): RealmQuery<GroupSummaryEntity> {
return GroupSummaryEntity.where(realm)
.process(GroupSummaryEntityFields.DISPLAY_NAME, queryParams.displayName)
.process(GroupSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)
}
} }

View file

@ -86,7 +86,7 @@ internal class DefaultPusherService @Inject constructor(private val context: Con
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun livePushers(): LiveData<List<Pusher>> { override fun getPushersLive(): LiveData<List<Pusher>> {
return monarchy.findAllMappedWithChanges( return monarchy.findAllMappedWithChanges(
{ realm -> PusherEntity.where(realm) }, { realm -> PusherEntity.where(realm) },
{ it.asDomain() } { it.asDomain() }

View file

@ -21,6 +21,7 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.VersioningState
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
@ -32,6 +33,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.findByAlias import im.vector.matrix.android.internal.database.query.findByAlias
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.query.process
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
@ -41,6 +43,7 @@ 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.fetchCopyMap import im.vector.matrix.android.internal.util.fetchCopyMap
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery
import javax.inject.Inject import javax.inject.Inject
internal class DefaultRoomService @Inject constructor(private val monarchy: Monarchy, internal class DefaultRoomService @Inject constructor(private val monarchy: Monarchy,
@ -86,30 +89,51 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
}) })
} }
override fun liveRoomSummaries(): LiveData<List<RoomSummary>> { override fun getRoomSummaries(queryParams: RoomSummaryQueryParams): List<RoomSummary> {
return monarchy.findAllMappedWithChanges( return monarchy.fetchAllMappedSync(
{ realm -> { roomSummariesQuery(it, queryParams) },
RoomSummaryEntity.where(realm)
.isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name)
},
{ roomSummaryMapper.map(it) } { roomSummaryMapper.map(it) }
) )
} }
override fun liveBreadcrumbs(): LiveData<List<RoomSummary>> { override fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>> {
return monarchy.findAllMappedWithChanges( return monarchy.findAllMappedWithChanges(
{ realm -> { roomSummariesQuery(it, queryParams) },
RoomSummaryEntity.where(realm)
.isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name)
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS)
.sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX)
},
{ roomSummaryMapper.map(it) } { roomSummaryMapper.map(it) }
) )
} }
private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery<RoomSummaryEntity> {
val query = RoomSummaryEntity.where(realm)
query.process(RoomSummaryEntityFields.DISPLAY_NAME, queryParams.displayName)
query.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias)
query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)
query.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name)
return query
}
override fun getBreadcrumbs(): List<RoomSummary> {
return monarchy.fetchAllMappedSync(
{ breadcrumbsQuery(it) },
{ roomSummaryMapper.map(it) }
)
}
override fun getBreadcrumbsLive(): LiveData<List<RoomSummary>> {
return monarchy.findAllMappedWithChanges(
{ breadcrumbsQuery(it) },
{ roomSummaryMapper.map(it) }
)
}
private fun breadcrumbsQuery(realm: Realm): RealmQuery<RoomSummaryEntity> {
return RoomSummaryEntity.where(realm)
.isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name)
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS)
.sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX)
}
override fun onRoomDisplayed(roomId: String): Cancelable { override fun onRoomDisplayed(roomId: String): Cancelable {
return updateBreadcrumbsTask return updateBreadcrumbsTask
.configureWith(UpdateBreadcrumbsTask.Params(roomId)) .configureWith(UpdateBreadcrumbsTask.Params(roomId))

View file

@ -20,10 +20,9 @@ import com.zhuinden.monarchy.Monarchy
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.RoomAvatarContent import im.vector.matrix.android.api.session.room.model.RoomAvatarContent
import im.vector.matrix.android.api.session.room.model.RoomMember
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.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields
import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.prev
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
@ -47,19 +46,15 @@ internal class RoomAvatarResolver @Inject constructor(private val monarchy: Mona
return@doWithRealm return@doWithRealm
} }
val roomMembers = RoomMembers(realm, roomId) val roomMembers = RoomMembers(realm, roomId)
val members = roomMembers.queryRoomMembersEvent().findAll() val members = roomMembers.queryActiveRoomMembersEvent().findAll()
// detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat) // detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat)
if (members.size == 1) { if (members.size == 1) {
res = members.firstOrNull()?.toRoomMember()?.avatarUrl res = members.firstOrNull()?.avatarUrl
} else if (members.size == 2) { } else if (members.size == 2) {
val firstOtherMember = members.where().notEqualTo(EventEntityFields.STATE_KEY, userId).findFirst() val firstOtherMember = members.where().notEqualTo(RoomMemberEntityFields.USER_ID, userId).findFirst()
res = firstOtherMember?.toRoomMember()?.avatarUrl res = firstOtherMember?.avatarUrl
} }
} }
return res return res
} }
private fun EventEntity?.toRoomMember(): RoomMember? {
return ContentMapper.map(this?.content).toModel<RoomMember>()
}
} }

View file

@ -24,8 +24,8 @@ 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.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.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.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.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.* import im.vector.matrix.android.internal.database.query.*
@ -93,10 +93,11 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev() val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()
val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev() val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev()
val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev() val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()
val encryptionEvent = EventEntity.where(realm, roomId, EventType.ENCRYPTION).prev()
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
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId) || !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
@ -105,18 +106,20 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>() roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>()
?.canonicalAlias ?.canonicalAlias
val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases ?: emptyList() val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases
?: emptyList()
roomSummaryEntity.aliases.clear() roomSummaryEntity.aliases.clear()
roomSummaryEntity.aliases.addAll(roomAliases) roomSummaryEntity.aliases.addAll(roomAliases)
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|") roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
roomSummaryEntity.isEncrypted = encryptionEvent != null
if (updateMembers) { if (updateMembers) {
val otherRoomMembers = RoomMembers(realm, roomId) val otherRoomMembers = RoomMembers(realm, roomId)
.queryRoomMembersEvent() .queryRoomMembersEvent()
.notEqualTo(EventEntityFields.STATE_KEY, userId) .notEqualTo(RoomMemberEntityFields.USER_ID, userId)
.findAll() .findAll()
.asSequence() .asSequence()
.map { it.stateKey } .map { it.userId }
roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)

View file

@ -21,18 +21,23 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.members.MembershipService
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
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.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
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.RoomMemberEntity
import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields
import im.vector.matrix.android.internal.query.process
import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
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.fetchCopied import im.vector.matrix.android.internal.util.fetchCopied
import io.realm.Realm
import io.realm.RealmQuery
internal class DefaultMembershipService @AssistedInject constructor(@Assisted private val roomId: String, internal class DefaultMembershipService @AssistedInject constructor(@Assisted private val roomId: String,
private val monarchy: Monarchy, private val monarchy: Monarchy,
@ -58,29 +63,44 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr
} }
override fun getRoomMember(userId: String): RoomMember? { override fun getRoomMember(userId: String): RoomMember? {
val eventEntity = monarchy.fetchCopied { val roomMemberEntity = monarchy.fetchCopied {
RoomMembers(it, roomId).queryRoomMemberEvent(userId).findFirst() RoomMembers(it, roomId).getLastRoomMember(userId)
} }
return eventEntity?.asDomain()?.content.toModel() return roomMemberEntity?.asDomain()
} }
override fun getRoomMemberIdsLive(): LiveData<List<String>> { override fun getRoomMembers(queryParams: RoomMemberQueryParams): List<RoomMember> {
return monarchy.findAllMappedWithChanges( return monarchy.fetchAllMappedSync(
{ {
RoomMembers(it, roomId).queryRoomMembersEvent() roomMembersQuery(it, queryParams)
}, },
{ {
it.stateKey!! it.asDomain()
} }
) )
} }
override fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData<List<RoomMember>> {
return monarchy.findAllMappedWithChanges(
{
roomMembersQuery(it, queryParams)
},
{
it.asDomain()
}
)
}
private fun roomMembersQuery(realm: Realm, queryParams: RoomMemberQueryParams): RealmQuery<RoomMemberEntity> {
return RoomMembers(realm, roomId).queryRoomMembersEvent()
.process(RoomMemberEntityFields.MEMBERSHIP_STR, queryParams.memberships)
.process(RoomMemberEntityFields.DISPLAY_NAME, queryParams.displayName)
}
override fun getNumberOfJoinedMembers(): Int { override fun getNumberOfJoinedMembers(): Int {
var result = 0 return Realm.getInstance(monarchy.realmConfiguration).use {
monarchy.runTransactionSync { RoomMembers(it, roomId).getNumberOfJoinedMembers()
result = RoomMembers(it, roomId).getNumberOfJoinedMembers()
} }
return result
} }
override fun invite(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable { override fun invite(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {

View file

@ -18,15 +18,14 @@ 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.internal.database.helper.addStateEvent import im.vector.matrix.android.internal.database.helper.addStateEvent
import im.vector.matrix.android.internal.database.helper.updateSenderData
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.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
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.session.user.UserEntityFactory
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 im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.Realm import io.realm.Realm
@ -44,7 +43,9 @@ internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Unit>
internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAPI: RoomAPI, internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAPI: RoomAPI,
private val monarchy: Monarchy, private val monarchy: Monarchy,
private val syncTokenStore: SyncTokenStore, private val syncTokenStore: SyncTokenStore,
private val roomSummaryUpdater: RoomSummaryUpdater private val roomSummaryUpdater: RoomSummaryUpdater,
private val roomMemberEventHandler: RoomMemberEventHandler,
private val timelineEventSenderVisitor: TimelineEventSenderVisitor
) : LoadRoomMembersTask { ) : LoadRoomMembersTask {
override suspend fun execute(params: LoadRoomMembersTask.Params) { override suspend fun execute(params: LoadRoomMembersTask.Params) {
@ -66,12 +67,11 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAP
for (roomMemberEvent in response.roomMemberEvents) { for (roomMemberEvent in response.roomMemberEvents) {
roomEntity.addStateEvent(roomMemberEvent) roomEntity.addStateEvent(roomMemberEvent)
UserEntityFactory.createOrNull(roomMemberEvent)?.also { roomMemberEventHandler.handle(realm, roomId, roomMemberEvent)
realm.insertOrUpdate(it)
}
} }
timelineEventSenderVisitor.clear()
roomEntity.chunks.flatMap { it.timelineEvents }.forEach { roomEntity.chunks.flatMap { it.timelineEvents }.forEach {
it.updateSenderData() timelineEventSenderVisitor.visit(it)
} }
roomEntity.areAllMembersLoaded = true roomEntity.areAllMembersLoaded = true
roomSummaryUpdater.update(realm, roomId, updateMembers = true) roomSummaryUpdater.update(realm, roomId, updateMembers = true)

View file

@ -23,9 +23,10 @@ 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.*
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.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.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomMemberEntity
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.prev
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
@ -75,43 +76,46 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
} }
val roomMembers = RoomMembers(realm, roomId) val roomMembers = RoomMembers(realm, roomId)
val loadedMembers = roomMembers.queryRoomMembersEvent().findAll() val activeMembers = roomMembers.queryActiveRoomMembersEvent().findAll()
if (roomEntity?.membership == Membership.INVITE) { if (roomEntity?.membership == Membership.INVITE) {
val inviteMeEvent = roomMembers.queryRoomMemberEvent(userId).findFirst() val inviteMeEvent = roomMembers.getLastStateEvent(userId)
val inviterId = inviteMeEvent?.sender val inviterId = inviteMeEvent?.sender
name = if (inviterId != null) { name = if (inviterId != null) {
val inviterMemberEvent = loadedMembers.where() activeMembers.where()
.equalTo(EventEntityFields.STATE_KEY, inviterId) .equalTo(RoomMemberEntityFields.USER_ID, inviterId)
.findFirst() .findFirst()
inviterMemberEvent?.toRoomMember()?.displayName ?.displayName
} else { } else {
context.getString(R.string.room_displayname_room_invite) context.getString(R.string.room_displayname_room_invite)
} }
} else if (roomEntity?.membership == Membership.JOIN) { } else if (roomEntity?.membership == Membership.JOIN) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
val otherMembersSubset: List<EventEntity> = if (roomSummary?.heroes?.isNotEmpty() == true) { val otherMembersSubset: List<RoomMemberEntity> = if (roomSummary?.heroes?.isNotEmpty() == true) {
roomSummary.heroes.mapNotNull { roomSummary.heroes.mapNotNull { userId ->
roomMembers.getStateEvent(it) roomMembers.getLastRoomMember(userId)?.takeIf {
it.membership == Membership.INVITE || it.membership == Membership.JOIN
}
} }
} else { } else {
loadedMembers.where() activeMembers.where()
.notEqualTo(EventEntityFields.STATE_KEY, userId) .notEqualTo(RoomMemberEntityFields.USER_ID, userId)
.limit(3) .limit(3)
.findAll() .findAll()
.createSnapshot()
} }
val otherMembersCount = roomMembers.getNumberOfMembers() - 1 val otherMembersCount = otherMembersSubset.count()
name = when (otherMembersCount) { name = when (otherMembersCount) {
0 -> context.getString(R.string.room_displayname_empty_room) 0 -> context.getString(R.string.room_displayname_empty_room)
1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers)
2 -> context.getString(R.string.room_displayname_two_members, 2 -> context.getString(R.string.room_displayname_two_members,
resolveRoomMemberName(otherMembersSubset[0], roomMembers), resolveRoomMemberName(otherMembersSubset[0], roomMembers),
resolveRoomMemberName(otherMembersSubset[1], roomMembers) resolveRoomMemberName(otherMembersSubset[1], roomMembers)
) )
else -> context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members, else -> context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members,
roomMembers.getNumberOfJoinedMembers() - 1, roomMembers.getNumberOfJoinedMembers() - 1,
resolveRoomMemberName(otherMembersSubset[0], roomMembers), resolveRoomMemberName(otherMembersSubset[0], roomMembers),
roomMembers.getNumberOfJoinedMembers() - 1) roomMembers.getNumberOfJoinedMembers() - 1)
} }
} }
return@doWithRealm return@doWithRealm
@ -119,19 +123,14 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
return name ?: roomId return name ?: roomId
} }
private fun resolveRoomMemberName(eventEntity: EventEntity?, private fun resolveRoomMemberName(roomMember: RoomMemberEntity?,
roomMembers: RoomMembers): String? { roomMembers: RoomMembers): String? {
if (eventEntity == null) return null if (roomMember == null) return null
val roomMember = eventEntity.toRoomMember() ?: return null
val isUnique = roomMembers.isUniqueDisplayName(roomMember.displayName) val isUnique = roomMembers.isUniqueDisplayName(roomMember.displayName)
return if (isUnique) { return if (isUnique) {
roomMember.displayName roomMember.displayName
} else { } else {
"${roomMember.displayName} (${eventEntity.stateKey})" "${roomMember.displayName} (${roomMember.userId})"
} }
} }
private fun EventEntity?.toRoomMember(): RoomMember? {
return ContentMapper.map(this?.content).toModel<RoomMember>()
}
} }

View file

@ -0,0 +1,36 @@
/*
* 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.membership
import im.vector.matrix.android.api.session.room.model.RoomMemberContent
import im.vector.matrix.android.internal.database.model.RoomMemberEntity
internal object RoomMemberEntityFactory {
fun create(roomId: String, userId: String, roomMember: RoomMemberContent): RoomMemberEntity {
val primaryKey = "${roomId}_$userId"
return RoomMemberEntity(
primaryKey = primaryKey,
userId = userId,
roomId = roomId,
displayName = roomMember.displayName ?: "",
avatarUrl = roomMember.avatarUrl ?: ""
).apply {
membership = roomMember.membership
}
}
}

View file

@ -0,0 +1,44 @@
/*
* 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.membership
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.Membership
import im.vector.matrix.android.api.session.room.model.RoomMemberContent
import im.vector.matrix.android.internal.session.user.UserEntityFactory
import io.realm.Realm
import javax.inject.Inject
internal class RoomMemberEventHandler @Inject constructor() {
fun handle(realm: Realm, roomId: String, event: Event): Boolean {
if (event.type != EventType.STATE_ROOM_MEMBER) {
return false
}
val roomMember = event.content.toModel<RoomMemberContent>() ?: return false
val userId = event.stateKey ?: return false
val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember)
realm.insertOrUpdate(roomMemberEntity)
if (roomMember.membership in Membership.activeMemberships()) {
val userEntity = UserEntityFactory.create(userId, roomMember)
realm.insertOrUpdate(userEntity)
}
return true
}
}

View file

@ -17,12 +17,10 @@
package im.vector.matrix.android.internal.session.room.membership 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.events.model.toModel
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.api.session.room.model.RoomMember import im.vector.matrix.android.internal.database.model.*
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.EventEntityFields import im.vector.matrix.android.internal.database.model.RoomMemberEntity
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.where import im.vector.matrix.android.internal.database.query.where
import io.realm.Realm import io.realm.Realm
@ -42,19 +40,18 @@ internal class RoomMembers(private val realm: Realm,
RoomSummaryEntity.where(realm, roomId).findFirst() RoomSummaryEntity.where(realm, roomId).findFirst()
} }
fun getStateEvent(userId: String): EventEntity? { fun getLastStateEvent(userId: String): EventEntity? {
return EventEntity return EventEntity
.where(realm, roomId, EventType.STATE_ROOM_MEMBER) .where(realm, roomId, EventType.STATE_ROOM_MEMBER)
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
.equalTo(EventEntityFields.STATE_KEY, userId) .equalTo(EventEntityFields.STATE_KEY, userId)
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
.findFirst() .findFirst()
} }
fun get(userId: String): RoomMember? { fun getLastRoomMember(userId: String): RoomMemberEntity? {
return getStateEvent(userId) return RoomMemberEntity
?.let { .where(realm, roomId, userId)
it.asDomain().content?.toModel<RoomMember>() .findFirst()
}
} }
fun isUniqueDisplayName(displayName: String?): Boolean { fun isUniqueDisplayName(displayName: String?): Boolean {
@ -69,36 +66,37 @@ internal class RoomMembers(private val realm: Realm,
.size == 1 .size == 1
} }
fun queryRoomMembersEvent(): RealmQuery<EventEntity> { fun queryRoomMembersEvent(): RealmQuery<RoomMemberEntity> {
return EventEntity return RoomMemberEntity.where(realm, roomId)
.where(realm, roomId, EventType.STATE_ROOM_MEMBER)
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
.isNotNull(EventEntityFields.STATE_KEY)
.distinct(EventEntityFields.STATE_KEY)
.isNotNull(EventEntityFields.CONTENT)
} }
fun queryJoinedRoomMembersEvent(): RealmQuery<EventEntity> { fun queryJoinedRoomMembersEvent(): RealmQuery<RoomMemberEntity> {
return queryRoomMembersEvent().contains(EventEntityFields.CONTENT, "\"membership\":\"join\"")
}
fun queryInvitedRoomMembersEvent(): RealmQuery<EventEntity> {
return queryRoomMembersEvent().contains(EventEntityFields.CONTENT, "\"membership\":\"invite\"")
}
fun queryRoomMemberEvent(userId: String): RealmQuery<EventEntity> {
return queryRoomMembersEvent() return queryRoomMembersEvent()
.equalTo(EventEntityFields.STATE_KEY, userId) .equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
}
fun queryInvitedRoomMembersEvent(): RealmQuery<RoomMemberEntity> {
return queryRoomMembersEvent()
.equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.INVITE.name)
}
fun queryActiveRoomMembersEvent(): RealmQuery<RoomMemberEntity> {
return queryRoomMembersEvent()
.beginGroup()
.equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.INVITE.name)
.or()
.equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
.endGroup()
} }
fun getNumberOfJoinedMembers(): Int { fun getNumberOfJoinedMembers(): Int {
return roomSummary?.joinedMembersCount return roomSummary?.joinedMembersCount
?: queryJoinedRoomMembersEvent().findAll().size ?: queryJoinedRoomMembersEvent().findAll().size
} }
fun getNumberOfInvitedMembers(): Int { fun getNumberOfInvitedMembers(): Int {
return roomSummary?.invitedMembersCount return roomSummary?.invitedMembersCount
?: queryInvitedRoomMembersEvent().findAll().size ?: queryInvitedRoomMembersEvent().findAll().size
} }
fun getNumberOfMembers(): Int { fun getNumberOfMembers(): Int {
@ -111,7 +109,7 @@ internal class RoomMembers(private val realm: Realm,
* @return a roomMember id list of joined or invited members. * @return a roomMember id list of joined or invited members.
*/ */
fun getActiveRoomMemberIds(): List<String> { fun getActiveRoomMemberIds(): List<String> {
return getRoomMemberIdsFiltered { it.membership == Membership.JOIN || it.membership == Membership.INVITE } return queryActiveRoomMembersEvent().findAll().map { it.userId }
} }
/** /**
@ -120,21 +118,6 @@ internal class RoomMembers(private val realm: Realm,
* @return a roomMember id list of joined members. * @return a roomMember id list of joined members.
*/ */
fun getJoinedRoomMemberIds(): List<String> { fun getJoinedRoomMemberIds(): List<String> {
return getRoomMemberIdsFiltered { it.membership == Membership.JOIN } return queryJoinedRoomMembersEvent().findAll().map { it.userId }
}
/* ==========================================================================================
* Private
* ========================================================================================== */
private fun getRoomMemberIdsFiltered(predicate: (RoomMember) -> Boolean): List<String> {
return RoomMembers(realm, roomId)
.queryRoomMembersEvent()
.findAll()
.map { it.asDomain() }
.associateBy { it.stateKey!! }
.filterValues { predicate(it.content.toModel<RoomMember>()!!) }
.keys
.toList()
} }
} }

View file

@ -20,7 +20,7 @@ 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.updateSenderData 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,7 +41,8 @@ internal interface PruneEventTask : Task<PruneEventTask.Params, Unit> {
) )
} }
internal class DefaultPruneEventTask @Inject constructor(private val monarchy: Monarchy) : PruneEventTask { internal class DefaultPruneEventTask @Inject constructor(private val monarchy: Monarchy,
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 ->
@ -65,12 +66,14 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst() val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
?: return ?: return
val allowedKeys = computeAllowedKeys(eventToPrune.type) val typeToPrune = eventToPrune.type
val stateKey = eventToPrune.stateKey
val allowedKeys = computeAllowedKeys(typeToPrune)
if (allowedKeys.isNotEmpty()) { if (allowedKeys.isNotEmpty()) {
val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) } val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) }
eventToPrune.content = ContentMapper.map(prunedContent) eventToPrune.content = ContentMapper.map(prunedContent)
} else { } else {
when (eventToPrune.type) { when (typeToPrune) {
EventType.ENCRYPTED, EventType.ENCRYPTED,
EventType.MESSAGE -> { EventType.MESSAGE -> {
Timber.d("REDACTION for message ${eventToPrune.eventId}") Timber.d("REDACTION for message ${eventToPrune.eventId}")
@ -94,11 +97,10 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
// } // }
} }
} }
if (eventToPrune.type == EventType.STATE_ROOM_MEMBER) { if (typeToPrune == EventType.STATE_ROOM_MEMBER && stateKey != null) {
timelineEventSenderVisitor.clear(roomId = eventToPrune.roomId, senderId = stateKey)
val timelineEventsToUpdate = TimelineEventEntity.findWithSenderMembershipEvent(realm, eventToPrune.eventId) val timelineEventsToUpdate = TimelineEventEntity.findWithSenderMembershipEvent(realm, eventToPrune.eventId)
for (timelineEvent in timelineEventsToUpdate) { timelineEventSenderVisitor.visit(timelineEventsToUpdate)
timelineEvent.updateSenderData()
}
} }
} }

View file

@ -215,7 +215,16 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain) return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
} }
override fun getEventSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>> { override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? {
return monarchy.fetchCopyMap(
{ EventAnnotationsSummaryEntity.where(it, eventId).findFirst() },
{ entity, _ ->
entity.asDomain()
}
)
}
override fun getEventAnnotationsSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>> {
val liveData = monarchy.findAllMappedWithChanges( val liveData = monarchy.findAllMappedWithChanges(
{ EventAnnotationsSummaryEntity.where(it, eventId) }, { EventAnnotationsSummaryEntity.where(it, eventId) },
{ it.asDomain() } { it.asDomain() }

View file

@ -21,7 +21,6 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.helper.deleteOnCascade 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.ChunkEntity
import im.vector.matrix.android.internal.database.model.ChunkEntityFields import im.vector.matrix.android.internal.database.model.ChunkEntityFields
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.where
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 im.vector.matrix.android.internal.util.awaitTransaction
@ -38,7 +37,7 @@ internal class DefaultClearUnlinkedEventsTask @Inject constructor(private val mo
monarchy.awaitTransaction { localRealm -> monarchy.awaitTransaction { localRealm ->
val unlinkedChunks = ChunkEntity val unlinkedChunks = ChunkEntity
.where(localRealm, roomId = params.roomId) .where(localRealm, roomId = params.roomId)
.equalTo("${ChunkEntityFields.TIMELINE_EVENTS.ROOT}.${EventEntityFields.IS_UNLINKED}", true) .equalTo(ChunkEntityFields.IS_UNLINKED, true)
.findAll() .findAll()
unlinkedChunks.forEach { unlinkedChunks.forEach {
it.deleteOnCascade() it.deleteOnCascade()

View file

@ -27,13 +27,7 @@ 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.ChunkEntity import im.vector.matrix.android.internal.database.model.*
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.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.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
@ -44,16 +38,10 @@ 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.OrderedCollectionChangeSet import io.realm.*
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 timber.log.Timber import timber.log.Timber
import java.util.Collections import java.util.*
import java.util.UUID 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.ArrayList
@ -77,11 +65,11 @@ internal class DefaultTimeline(
private val hiddenReadReceipts: TimelineHiddenReadReceipts private val hiddenReadReceipts: TimelineHiddenReadReceipts
) : Timeline, TimelineHiddenReadReceipts.Delegate { ) : Timeline, TimelineHiddenReadReceipts.Delegate {
private companion object { companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
} }
private val listeners = ArrayList<Timeline.Listener>() private val listeners = CopyOnWriteArrayList<Timeline.Listener>()
private val isStarted = AtomicBoolean(false) private val isStarted = AtomicBoolean(false)
private val isReady = AtomicBoolean(false) private val isReady = AtomicBoolean(false)
private val mainHandler = createUIHandler() private val mainHandler = createUIHandler()
@ -113,11 +101,7 @@ internal class DefaultTimeline(
if (!results.isLoaded || !results.isValid) { if (!results.isLoaded || !results.isValid) {
return@OrderedRealmCollectionChangeListener return@OrderedRealmCollectionChangeListener
} }
if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) { handleUpdates(changeSet)
handleInitialLoad()
} else {
handleUpdates(changeSet)
}
} }
private val relationsListener = OrderedRealmCollectionChangeListener<RealmResults<EventAnnotationsSummaryEntity>> { collection, changeSet -> private val relationsListener = OrderedRealmCollectionChangeListener<RealmResults<EventAnnotationsSummaryEntity>> { collection, changeSet ->
@ -179,8 +163,9 @@ internal class DefaultTimeline(
nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING).findAll() nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING).findAll()
filteredEvents = nonFilteredEvents.where() filteredEvents = nonFilteredEvents.where()
.filterEventsWithSettings() .filterEventsWithSettings()
.findAllAsync() .findAll()
.also { it.addChangeListener(eventsChangeListener) } handleInitialLoad()
filteredEvents.addChangeListener(eventsChangeListener)
eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId) eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId)
.findAllAsync() .findAllAsync()
@ -288,20 +273,20 @@ internal class DefaultTimeline(
return hasMoreInCache(direction) || !hasReachedEnd(direction) return hasMoreInCache(direction) || !hasReachedEnd(direction)
} }
override fun addListener(listener: Timeline.Listener) = synchronized(listeners) { override fun addListener(listener: Timeline.Listener): Boolean {
if (listeners.contains(listener)) { if (listeners.contains(listener)) {
return false return false
} }
listeners.add(listener).also { return listeners.add(listener).also {
postSnapshot() postSnapshot()
} }
} }
override fun removeListener(listener: Timeline.Listener) = synchronized(listeners) { override fun removeListener(listener: Timeline.Listener): Boolean {
listeners.remove(listener) return listeners.remove(listener)
} }
override fun removeAllListeners() = synchronized(listeners) { override fun removeAllListeners() {
listeners.clear() listeners.clear()
} }
@ -402,14 +387,14 @@ internal class DefaultTimeline(
private fun getState(direction: Timeline.Direction): State { private fun getState(direction: Timeline.Direction): State {
return when (direction) { return when (direction) {
Timeline.Direction.FORWARDS -> forwardsState.get() Timeline.Direction.FORWARDS -> forwardsState.get()
Timeline.Direction.BACKWARDS -> backwardsState.get() Timeline.Direction.BACKWARDS -> backwardsState.get()
} }
} }
private fun updateState(direction: Timeline.Direction, update: (State) -> State) { private fun updateState(direction: Timeline.Direction, update: (State) -> State) {
val stateReference = when (direction) { val stateReference = when (direction) {
Timeline.Direction.FORWARDS -> forwardsState Timeline.Direction.FORWARDS -> forwardsState
Timeline.Direction.BACKWARDS -> backwardsState Timeline.Direction.BACKWARDS -> backwardsState
} }
val currentValue = stateReference.get() val currentValue = stateReference.get()
@ -508,10 +493,10 @@ internal class DefaultTimeline(
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) {
TokenChunkEventPersistor.Result.SUCCESS -> { TokenChunkEventPersistor.Result.SUCCESS -> {
Timber.v("Success fetching $limit items $direction from pagination request") Timber.v("Success fetching $limit items $direction from pagination request")
} }
TokenChunkEventPersistor.Result.REACHED_END -> { TokenChunkEventPersistor.Result.REACHED_END -> {
postSnapshot() postSnapshot()
} }
TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->
@ -656,10 +641,8 @@ internal class DefaultTimeline(
updateLoadingStates(filteredEvents) updateLoadingStates(filteredEvents)
val snapshot = createSnapshot() val snapshot = createSnapshot()
val runnable = Runnable { val runnable = Runnable {
synchronized(listeners) { listeners.forEach {
listeners.forEach { it.onTimelineUpdated(snapshot)
it.onTimelineUpdated(snapshot)
}
} }
} }
debouncer.debounce("post_snapshot", runnable, 50) debouncer.debounce("post_snapshot", runnable, 50)
@ -671,10 +654,8 @@ internal class DefaultTimeline(
return return
} }
val runnable = Runnable { val runnable = Runnable {
synchronized(listeners) { listeners.forEach {
listeners.forEach { it.onTimelineFailure(throwable)
it.onTimelineFailure(throwable)
}
} }
} }
mainHandler.post(runnable) mainHandler.post(runnable)

View file

@ -18,17 +18,12 @@ 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.internal.database.helper.*
import im.vector.matrix.android.internal.database.helper.add
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.deleteOnCascade
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.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.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.user.UserEntityFactory
import im.vector.matrix.android.internal.util.awaitTransaction import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
import timber.log.Timber import timber.log.Timber
@ -37,7 +32,8 @@ 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>
@ -136,27 +132,22 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
// 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 // otherwise we create a whole new one which is unlinked (not live)
var 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) ?: ChunkEntity.create(realm, prevToken, nextToken, isUnlinked = true)
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) { if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
Timber.v("Reach end of $roomId") Timber.v("Reach end of $roomId")
currentChunk.isLastBackward = true currentChunk.isLastBackward = true
} else if (!shouldSkip) { } else if (!shouldSkip) {
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
val eventIds = ArrayList<String>(receivedChunk.events.size) val timelineEvents = receivedChunk.events.mapNotNull {
for (event in receivedChunk.events) { currentChunk.add(roomId, it, direction)
event.eventId?.also { eventIds.add(it) }
currentChunk.add(roomId, event, direction, isUnlinked = currentChunk.isUnlinked())
UserEntityFactory.createOrNull(event)?.also {
realm.insertOrUpdate(it)
}
} }
// Then we merge chunks if needed // Then we merge chunks if needed
if (currentChunk != prevChunk && prevChunk != null) { if (currentChunk != prevChunk && prevChunk != null) {
@ -174,12 +165,9 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
} }
roomEntity.addOrUpdate(currentChunk) roomEntity.addOrUpdate(currentChunk)
for (stateEvent in receivedChunk.stateEvents) { for (stateEvent in receivedChunk.stateEvents) {
roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked()) roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked)
UserEntityFactory.createOrNull(stateEvent)?.also {
realm.insertOrUpdate(it)
}
} }
currentChunk.updateSenderDataFor(eventIds) timelineEventSenderVisitor.visit(timelineEvents)
} }
} }
return if (receivedChunk.events.isEmpty()) { return if (receivedChunk.events.isEmpty()) {
@ -200,11 +188,13 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
// We always merge the bottom chunk into top chunk, so we are always merging backwards // We always merge the bottom chunk into top chunk, so we are always merging backwards
Timber.v("Merge ${currentChunk.prevToken} | ${currentChunk.nextToken} with ${otherChunk.prevToken} | ${otherChunk.nextToken}") Timber.v("Merge ${currentChunk.prevToken} | ${currentChunk.nextToken} with ${otherChunk.prevToken} | ${otherChunk.nextToken}")
return if (direction == PaginationDirection.BACKWARDS && !otherChunk.isLastForward) { return if (direction == PaginationDirection.BACKWARDS && !otherChunk.isLastForward) {
currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS) val events = currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS)
timelineEventSenderVisitor.visit(events)
roomEntity.deleteOnCascade(otherChunk) roomEntity.deleteOnCascade(otherChunk)
currentChunk currentChunk
} else { } else {
otherChunk.merge(roomEntity.roomId, currentChunk, PaginationDirection.BACKWARDS) val events = otherChunk.merge(roomEntity.roomId, currentChunk, PaginationDirection.BACKWARDS)
timelineEventSenderVisitor.visit(events)
roomEntity.deleteOnCascade(currentChunk) roomEntity.deleteOnCascade(currentChunk)
otherChunk otherChunk
} }

View file

@ -27,16 +27,17 @@ import im.vector.matrix.android.internal.database.helper.*
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.EventEntityFields import im.vector.matrix.android.internal.database.model.EventEntityFields
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.find import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
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.DefaultInitialSyncProgressService import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
import im.vector.matrix.android.internal.session.mapWithProgress import im.vector.matrix.android.internal.session.mapWithProgress
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.membership.RoomMemberEventHandler
import im.vector.matrix.android.internal.session.room.read.FullyReadContent import im.vector.matrix.android.internal.session.room.read.FullyReadContent
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.internal.session.sync.model.* import im.vector.matrix.android.internal.session.sync.model.*
import im.vector.matrix.android.internal.session.user.UserEntityFactory
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
import timber.log.Timber import timber.log.Timber
@ -46,7 +47,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private val roomSummaryUpdater: RoomSummaryUpdater, private val roomSummaryUpdater: RoomSummaryUpdater,
private val roomTagHandler: RoomTagHandler, private val roomTagHandler: RoomTagHandler,
private val roomFullyReadHandler: RoomFullyReadHandler, private val roomFullyReadHandler: RoomFullyReadHandler,
private val cryptoService: DefaultCryptoService) { private val cryptoService: DefaultCryptoService,
private val roomMemberEventHandler: RoomMemberEventHandler,
private val timelineEventSenderVisitor: TimelineEventSenderVisitor) {
sealed class HandlingStrategy { sealed class HandlingStrategy {
data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy() data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
@ -119,9 +122,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex) roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex)
// Give info to crypto module // Give info to crypto module
cryptoService.onStateEvent(roomId, event) cryptoService.onStateEvent(roomId, event)
UserEntityFactory.createOrNull(event)?.also { roomMemberEventHandler.handle(realm, roomId, event)
realm.insertOrUpdate(it)
}
} }
} }
if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) { if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) {
@ -189,11 +190,13 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
} }
lastChunk?.isLastForward = false lastChunk?.isLastForward = false
chunkEntity.isLastForward = true chunkEntity.isLastForward = true
chunkEntity.isUnlinked = false
val eventIds = ArrayList<String>(eventList.size) val timelineEvents = ArrayList<TimelineEventEntity>(eventList.size)
for (event in eventList) { for (event in eventList) {
event.eventId?.also { eventIds.add(it) } chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)?.also {
chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset) timelineEvents.add(it)
}
// Give info to crypto module // Give info to crypto module
cryptoService.onLiveEvent(roomEntity.roomId, event) cryptoService.onLiveEvent(roomEntity.roomId, event)
// Try to remove local echo // Try to remove local echo
@ -206,11 +209,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
Timber.v("Can't find corresponding local echo for tx:$it") Timber.v("Can't find corresponding local echo for tx:$it")
} }
} }
UserEntityFactory.createOrNull(event)?.also { roomMemberEventHandler.handle(realm, roomEntity.roomId, event)
realm.insertOrUpdate(it)
}
} }
chunkEntity.updateSenderDataFor(eventIds) timelineEventSenderVisitor.visit(timelineEvents)
return chunkEntity return chunkEntity
} }

View file

@ -20,7 +20,7 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.pushrules.RuleScope import im.vector.matrix.android.api.pushrules.RuleScope
import im.vector.matrix.android.api.pushrules.RuleSetKey import im.vector.matrix.android.api.pushrules.RuleSetKey
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.RoomMember import im.vector.matrix.android.api.session.room.model.RoomMemberContent
import im.vector.matrix.android.internal.database.mapper.PushRulesMapper import im.vector.matrix.android.internal.database.mapper.PushRulesMapper
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.*
@ -69,9 +69,9 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc
var hasUpdate = false var hasUpdate = false
monarchy.doWithRealm { realm -> monarchy.doWithRealm { realm ->
invites.forEach { (roomId, _) -> invites.forEach { (roomId, _) ->
val myUserStateEvent = RoomMembers(realm, roomId).getStateEvent(userId) val myUserStateEvent = RoomMembers(realm, roomId).getLastStateEvent(userId)
val inviterId = myUserStateEvent?.sender val inviterId = myUserStateEvent?.sender
val myUserRoomMember: RoomMember? = myUserStateEvent?.let { it.asDomain().content?.toModel() } val myUserRoomMember: RoomMemberContent? = myUserStateEvent?.let { it.asDomain().content?.toModel() }
val isDirect = myUserRoomMember?.isDirect val isDirect = myUserRoomMember?.isDirect
if (inviterId != null && inviterId != userId && isDirect == true) { if (inviterId != null && inviterId != userId && isDirect == true) {
directChats directChats

View file

@ -112,7 +112,7 @@ abstract class SyncService : Service() {
try { try {
syncTask.execute(params) syncTask.execute(params)
// Start sync if we were doing an initial sync and the syncThread is not launched yet // Start sync if we were doing an initial sync and the syncThread is not launched yet
if (isInitialSync && session.syncState().value == SyncState.Idle) { if (isInitialSync && session.getSyncStateLive().value == SyncState.Idle) {
val isForeground = !backgroundDetectionObserver.isInBackground val isForeground = !backgroundDetectionObserver.isInBackground
session.startSync(isForeground) session.startSync(isForeground)
} }

View file

@ -70,7 +70,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
return userEntity.asDomain() return userEntity.asDomain()
} }
override fun liveUser(userId: String): LiveData<Optional<User>> { override fun getUserLive(userId: String): LiveData<Optional<User>> {
val liveData = monarchy.findAllMappedWithChanges( val liveData = monarchy.findAllMappedWithChanges(
{ UserEntity.where(it, userId) }, { UserEntity.where(it, userId) },
{ it.asDomain() } { it.asDomain() }
@ -80,7 +80,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
} }
} }
override fun liveUsers(): LiveData<List<User>> { override fun getUsersLive(): LiveData<List<User>> {
return monarchy.findAllMappedWithChanges( return monarchy.findAllMappedWithChanges(
{ realm -> { realm ->
realm.where(UserEntity::class.java) realm.where(UserEntity::class.java)
@ -91,7 +91,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
) )
} }
override fun livePagedUsers(filter: String?): LiveData<PagedList<User>> { override fun getPagedUsersLive(filter: String?): LiveData<PagedList<User>> {
realmDataSourceFactory.updateQuery { realm -> realmDataSourceFactory.updateQuery { realm ->
val query = realm.where(UserEntity::class.java) val query = realm.where(UserEntity::class.java)
if (filter.isNullOrEmpty()) { if (filter.isNullOrEmpty()) {
@ -121,7 +121,7 @@ internal class DefaultUserService @Inject constructor(private val monarchy: Mona
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun liveIgnoredUsers(): LiveData<List<User>> { override fun getIgnoredUsersLive(): LiveData<List<User>> {
return monarchy.findAllMappedWithChanges( return monarchy.findAllMappedWithChanges(
{ realm -> { realm ->
realm.where(IgnoredUserEntity::class.java) realm.where(IgnoredUserEntity::class.java)

View file

@ -16,27 +16,16 @@
package im.vector.matrix.android.internal.session.user package im.vector.matrix.android.internal.session.user
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.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.internal.database.model.UserEntity import im.vector.matrix.android.internal.database.model.UserEntity
internal object UserEntityFactory { internal object UserEntityFactory {
fun createOrNull(event: Event): UserEntity? { fun create(userId: String, roomMember: RoomMemberContent): UserEntity {
if (event.type != EventType.STATE_ROOM_MEMBER) { return UserEntity(
return null userId = userId,
} displayName = roomMember.displayName ?: "",
val roomMember = event.content.toModel<RoomMember>() ?: return null avatarUrl = roomMember.avatarUrl ?: ""
// We only use JOIN and INVITED memberships to create User data
if (roomMember.membership != Membership.JOIN && roomMember.membership != Membership.INVITE) {
return null
}
return UserEntity(event.stateKey ?: "",
roomMember.displayName ?: "",
roomMember.avatarUrl ?: ""
) )
} }
} }

View file

@ -21,7 +21,7 @@ import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.RoomService
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.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomMemberContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@ -40,7 +40,7 @@ class PushrulesConditionTest {
content = MessageTextContent("m.text", "Yo wtf?").toContent(), content = MessageTextContent("m.text", "Yo wtf?").toContent(),
originServerTs = 0) originServerTs = 0)
val rm = RoomMember( val rm = RoomMemberContent(
Membership.INVITE, Membership.INVITE,
displayName = "Foo", displayName = "Foo",
avatarUrl = "mxc://matrix.org/EqMZYbREvHXvYFyfxOlkf" avatarUrl = "mxc://matrix.org/EqMZYbREvHXvYFyfxOlkf"
@ -72,7 +72,7 @@ class PushrulesConditionTest {
type = "m.room.member", type = "m.room.member",
eventId = "mx0", eventId = "mx0",
stateKey = "@foo:matrix.org", stateKey = "@foo:matrix.org",
content = RoomMember( content = RoomMemberContent(
Membership.INVITE, Membership.INVITE,
displayName = "Foo", displayName = "Foo",
avatarUrl = "mxc://matrix.org/EqMZYbREvHXvYFyfxOlkf" avatarUrl = "mxc://matrix.org/EqMZYbREvHXvYFyfxOlkf"

View file

@ -218,8 +218,8 @@ android {
dependencies { dependencies {
def epoxy_version = '3.8.0' def epoxy_version = '3.9.0'
def fragment_version = '1.2.0-rc01' def fragment_version = '1.2.0-rc04'
def arrow_version = "0.8.2" def arrow_version = "0.8.2"
def coroutines_version = "1.3.2" def coroutines_version = "1.3.2"
def markwon_version = '4.1.2' def markwon_version = '4.1.2'
@ -227,7 +227,7 @@ dependencies {
def glide_version = '4.10.0' def glide_version = '4.10.0'
def moshi_version = '1.8.0' def moshi_version = '1.8.0'
def daggerVersion = '2.24' def daggerVersion = '2.24'
def autofill_version = "1.0.0-rc01" def autofill_version = "1.0.0"
implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android")
implementation project(":matrix-sdk-android-rx") implementation project(":matrix-sdk-android-rx")
@ -238,11 +238,11 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "androidx.recyclerview:recyclerview:1.2.0-alpha01"
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.1.0'
implementation "androidx.fragment:fragment:$fragment_version" implementation "androidx.fragment:fragment:$fragment_version"
implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation "androidx.fragment:fragment-ktx:$fragment_version"
//Do not use beta2 at the moment, as it breaks things implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta1'
implementation 'androidx.core:core-ktx:1.1.0' implementation 'androidx.core:core-ktx:1.1.0'
implementation "org.threeten:threetenbp:1.4.0:no-tzdb" implementation "org.threeten:threetenbp:1.4.0:no-tzdb"
@ -275,10 +275,10 @@ dependencies {
implementation 'com.airbnb.android:mvrx:1.3.0' implementation 'com.airbnb.android:mvrx:1.3.0'
// Work // Work
implementation "androidx.work:work-runtime-ktx:2.3.0-alpha01" implementation "androidx.work:work-runtime-ktx:2.3.0-beta02"
// Paging // Paging
implementation "androidx.paging:paging-runtime-ktx:2.1.0" implementation "androidx.paging:paging-runtime-ktx:2.1.1"
// Functional Programming // Functional Programming
implementation "io.arrow-kt:arrow-core:$arrow_version" implementation "io.arrow-kt:arrow-core:$arrow_version"
@ -288,7 +288,7 @@ dependencies {
// UI // UI
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.google.android.material:material:1.1.0-beta01' implementation 'com.google.android.material:material:1.2.0-alpha03'
implementation 'me.gujun.android:span:1.7' implementation 'me.gujun.android:span:1.7'
implementation "io.noties.markwon:core:$markwon_version" implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:html:$markwon_version" implementation "io.noties.markwon:html:$markwon_version"

View file

@ -22,6 +22,7 @@ import androidx.lifecycle.OnLifecycleEvent
import arrow.core.Option import arrow.core.Option
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.features.home.HomeRoomListDataSource import im.vector.riotx.features.home.HomeRoomListDataSource
import im.vector.riotx.features.home.group.ALL_COMMUNITIES_GROUP_ID import im.vector.riotx.features.home.group.ALL_COMMUNITIES_GROUP_ID
@ -65,7 +66,8 @@ class AppStateHandler @Inject constructor(
sessionDataSource.observe() sessionDataSource.observe()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.switchMap { .switchMap {
it.orNull()?.rx()?.liveRoomSummaries() val query = roomSummaryQueryParams {}
it.orNull()?.rx()?.liveRoomSummaries(query)
?: Observable.just(emptyList()) ?: Observable.just(emptyList())
} }
.throttleLast(300, TimeUnit.MILLISECONDS), .throttleLast(300, TimeUnit.MILLISECONDS),

View file

@ -63,7 +63,8 @@ import im.vector.riotx.features.ui.UiStateRepository
ViewModelModule::class, ViewModelModule::class,
FragmentModule::class, FragmentModule::class,
HomeModule::class, HomeModule::class,
RoomListModule::class RoomListModule::class,
ScreenModule::class
] ]
) )
@ScreenScope @ScreenScope

View file

@ -17,6 +17,7 @@
package im.vector.riotx.core.di package im.vector.riotx.core.di
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
@ -27,4 +28,9 @@ object ScreenModule {
@Provides @Provides
@JvmStatic @JvmStatic
fun providesGlideRequests(context: AppCompatActivity) = GlideApp.with(context) fun providesGlideRequests(context: AppCompatActivity) = GlideApp.with(context)
@Provides
@JvmStatic
@ScreenScope
fun providesSharedViewPool() = RecyclerView.RecycledViewPool()
} }

View file

@ -44,6 +44,7 @@ import im.vector.riotx.features.notifications.*
import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.VectorFileLogger import im.vector.riotx.features.rageshake.VectorFileLogger
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.reactions.data.EmojiDataSource
import im.vector.riotx.features.session.SessionListener import im.vector.riotx.features.session.SessionListener
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.share.ShareRoomListDataSource import im.vector.riotx.features.share.ShareRoomListDataSource
@ -124,6 +125,8 @@ interface VectorComponent {
fun uiStateRepository(): UiStateRepository fun uiStateRepository(): UiStateRepository
fun emojiDataSource(): EmojiDataSource
@Component.Factory @Component.Factory
interface Factory { interface Factory {
fun create(@BindsInstance context: Context): VectorComponent fun create(@BindsInstance context: Context): VectorComponent

View file

@ -18,14 +18,25 @@ package im.vector.riotx.core.epoxy
import com.airbnb.epoxy.EpoxyModelWithHolder import com.airbnb.epoxy.EpoxyModelWithHolder
import com.airbnb.epoxy.VisibilityState import com.airbnb.epoxy.VisibilityState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
/** /**
* EpoxyModelWithHolder which can listen to visibility state change * EpoxyModelWithHolder which can listen to visibility state change
*/ */
abstract class VectorEpoxyModel<H : VectorEpoxyHolder> : EpoxyModelWithHolder<H>() { abstract class VectorEpoxyModel<H : VectorEpoxyHolder> : EpoxyModelWithHolder<H>() {
protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private var onModelVisibilityStateChangedListener: OnVisibilityStateChangedListener? = null private var onModelVisibilityStateChangedListener: OnVisibilityStateChangedListener? = null
override fun unbind(holder: H) {
coroutineScope.coroutineContext.cancelChildren()
super.unbind(holder)
}
override fun onVisibilityStateChanged(visibilityState: Int, view: H) { override fun onVisibilityStateChanged(visibilityState: Int, view: H) {
onModelVisibilityStateChangedListener?.onVisibilityStateChanged(visibilityState) onModelVisibilityStateChangedListener?.onVisibilityStateChanged(visibilityState)
super.onVisibilityStateChanged(visibilityState, view) super.onVisibilityStateChanged(visibilityState, view)

View file

@ -51,7 +51,7 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
holder.sender.setTextOrHide(matrixItem.displayName) holder.sender.setTextOrHide(matrixItem.displayName)
holder.body.movementMethod = movementMethod holder.body.movementMethod = movementMethod
holder.body.text = body holder.body.text = body
body.findPillsAndProcess { it.bind(holder.body) } body.findPillsAndProcess(coroutineScope) { it.bind(holder.body) }
holder.timestamp.setTextOrHide(time) holder.timestamp.setTextOrHide(time)
} }

View file

@ -26,9 +26,13 @@ import com.airbnb.epoxy.EpoxyController
*/ */
fun RecyclerView.configureWith(epoxyController: EpoxyController, fun RecyclerView.configureWith(epoxyController: EpoxyController,
itemAnimator: RecyclerView.ItemAnimator? = null, itemAnimator: RecyclerView.ItemAnimator? = null,
viewPool: RecyclerView.RecycledViewPool? = null,
showDivider: Boolean = false, showDivider: Boolean = false,
hasFixedSize: Boolean = true) { hasFixedSize: Boolean = true) {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false).apply {
recycleChildrenOnDetach = viewPool != null
}
setRecycledViewPool(viewPool)
itemAnimator?.let { this.itemAnimator = it } itemAnimator?.let { this.itemAnimator = it }
if (showDivider) { if (showDivider) {
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))

View file

@ -45,15 +45,13 @@ class VectorSyncService : SyncService() {
} }
override fun onStart(isInitialSync: Boolean) { override fun onStart(isInitialSync: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val notificationSubtitleRes = if (isInitialSync) {
val notificationSubtitleRes = if (isInitialSync) { R.string.notification_initial_sync
R.string.notification_initial_sync } else {
} else { R.string.notification_listening_for_events
R.string.notification_listening_for_events
}
val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false)
startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
} }
val notification = notificationUtils.buildForegroundServiceNotification(notificationSubtitleRes, false)
startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
} }
override fun onRescheduleAsked(userId: String, isInitialSync: Boolean, delay: Long) { override fun onRescheduleAsked(userId: String, isInitialSync: Boolean, delay: Long) {

View file

@ -37,10 +37,6 @@ class AutocompleteEmojiController @Inject constructor(
} }
} }
init {
fontProvider.addListener(fontProviderListener)
}
var listener: AutocompleteClickListener<String>? = null var listener: AutocompleteClickListener<String>? = null
override fun buildModels(data: List<EmojiItem>?) { override fun buildModels(data: List<EmojiItem>?) {
@ -71,6 +67,10 @@ class AutocompleteEmojiController @Inject constructor(
} }
} }
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
fontProvider.addListener(fontProviderListener)
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView) super.onDetachedFromRecyclerView(recyclerView)
fontProvider.removeListener(fontProviderListener) fontProvider.removeListener(fontProviderListener)

View file

@ -18,19 +18,19 @@ package im.vector.riotx.features.autocomplete.group
import android.content.Context import android.content.Context
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Success
import com.otaliastudios.autocomplete.RecyclerViewPresenter import com.otaliastudios.autocomplete.RecyclerViewPresenter
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.groupSummaryQueryParams
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotx.features.autocomplete.AutocompleteClickListener import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import javax.inject.Inject import javax.inject.Inject
class AutocompleteGroupPresenter @Inject constructor(context: Context, class AutocompleteGroupPresenter @Inject constructor(context: Context,
private val controller: AutocompleteGroupController private val controller: AutocompleteGroupController,
private val session: Session
) : RecyclerViewPresenter<GroupSummary>(context), AutocompleteClickListener<GroupSummary> { ) : RecyclerViewPresenter<GroupSummary>(context), AutocompleteClickListener<GroupSummary> {
var callback: Callback? = null
init { init {
controller.listener = this controller.listener = this
} }
@ -46,16 +46,16 @@ class AutocompleteGroupPresenter @Inject constructor(context: Context,
} }
override fun onQuery(query: CharSequence?) { override fun onQuery(query: CharSequence?) {
callback?.onQueryGroups(query) val queryParams = groupSummaryQueryParams {
} displayName = if (query.isNullOrBlank()) {
QueryStringValue.IsNotEmpty
fun render(groups: Async<List<GroupSummary>>) { } else {
if (groups is Success) { QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
controller.setData(groups()) }
} }
} val groups = session.getGroupSummaries(queryParams)
.asSequence()
interface Callback { .sortedBy { it.displayName }
fun onQueryGroups(query: CharSequence?) controller.setData(groups.toList())
} }
} }

View file

@ -14,23 +14,23 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.autocomplete.user package im.vector.riotx.features.autocomplete.member
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.features.autocomplete.AutocompleteClickListener import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import im.vector.riotx.features.autocomplete.autocompleteMatrixItem import im.vector.riotx.features.autocomplete.autocompleteMatrixItem
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject import javax.inject.Inject
class AutocompleteUserController @Inject constructor() : TypedEpoxyController<List<User>>() { class AutocompleteMemberController @Inject constructor() : TypedEpoxyController<List<RoomMember>>() {
var listener: AutocompleteClickListener<User>? = null var listener: AutocompleteClickListener<RoomMember>? = null
@Inject lateinit var avatarRenderer: AvatarRenderer @Inject lateinit var avatarRenderer: AvatarRenderer
override fun buildModels(data: List<User>?) { override fun buildModels(data: List<RoomMember>?) {
if (data.isNullOrEmpty()) { if (data.isNullOrEmpty()) {
return return
} }

View file

@ -0,0 +1,72 @@
/*
* 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.riotx.features.autocomplete.member
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import com.otaliastudios.autocomplete.RecyclerViewPresenter
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
class AutocompleteMemberPresenter @AssistedInject constructor(context: Context,
@Assisted val roomId: String,
private val session: Session,
private val controller: AutocompleteMemberController
) : RecyclerViewPresenter<RoomMember>(context), AutocompleteClickListener<RoomMember> {
private val room = session.getRoom(roomId)!!
init {
controller.listener = this
}
@AssistedInject.Factory
interface Factory {
fun create(roomId: String): AutocompleteMemberPresenter
}
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
// Also remove animation
recyclerView?.itemAnimator = null
return controller.adapter
}
override fun onItemClick(t: RoomMember) {
dispatchClick(t)
}
override fun onQuery(query: CharSequence?) {
val queryParams = roomMemberQueryParams {
displayName = if (query.isNullOrBlank()) {
QueryStringValue.IsNotEmpty
} else {
QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
}
memberships = listOf(Membership.JOIN)
}
val members = room.getRoomMembers(queryParams)
.asSequence()
.sortedBy { it.displayName }
controller.setData(members.toList())
}
}

View file

@ -24,12 +24,10 @@ import im.vector.riotx.features.autocomplete.autocompleteMatrixItem
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject import javax.inject.Inject
class AutocompleteRoomController @Inject constructor() : TypedEpoxyController<List<RoomSummary>>() { class AutocompleteRoomController @Inject constructor(private val avatarRenderer: AvatarRenderer) : TypedEpoxyController<List<RoomSummary>>() {
var listener: AutocompleteClickListener<RoomSummary>? = null var listener: AutocompleteClickListener<RoomSummary>? = null
@Inject lateinit var avatarRenderer: AvatarRenderer
override fun buildModels(data: List<RoomSummary>?) { override fun buildModels(data: List<RoomSummary>?) {
if (data.isNullOrEmpty()) { if (data.isNullOrEmpty()) {
return return

View file

@ -18,19 +18,19 @@ package im.vector.riotx.features.autocomplete.room
import android.content.Context import android.content.Context
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Success
import com.otaliastudios.autocomplete.RecyclerViewPresenter import com.otaliastudios.autocomplete.RecyclerViewPresenter
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
import im.vector.riotx.features.autocomplete.AutocompleteClickListener import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import javax.inject.Inject import javax.inject.Inject
class AutocompleteRoomPresenter @Inject constructor(context: Context, class AutocompleteRoomPresenter @Inject constructor(context: Context,
private val controller: AutocompleteRoomController private val controller: AutocompleteRoomController,
private val session: Session
) : RecyclerViewPresenter<RoomSummary>(context), AutocompleteClickListener<RoomSummary> { ) : RecyclerViewPresenter<RoomSummary>(context), AutocompleteClickListener<RoomSummary> {
var callback: Callback? = null
init { init {
controller.listener = this controller.listener = this
} }
@ -46,16 +46,16 @@ class AutocompleteRoomPresenter @Inject constructor(context: Context,
} }
override fun onQuery(query: CharSequence?) { override fun onQuery(query: CharSequence?) {
callback?.onQueryRooms(query) val queryParams = roomSummaryQueryParams {
} canonicalAlias = if (query.isNullOrBlank()) {
QueryStringValue.IsNotNull
fun render(rooms: Async<List<RoomSummary>>) { } else {
if (rooms is Success) { QueryStringValue.Contains(query.toString(), QueryStringValue.Case.INSENSITIVE)
controller.setData(rooms()) }
} }
} val rooms = session.getRoomSummaries(queryParams)
.asSequence()
interface Callback { .sortedBy { it.displayName }
fun onQueryRooms(query: CharSequence?) controller.setData(rooms.toList())
} }
} }

View file

@ -1,61 +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.riotx.features.autocomplete.user
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Success
import com.otaliastudios.autocomplete.RecyclerViewPresenter
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import javax.inject.Inject
class AutocompleteUserPresenter @Inject constructor(context: Context,
private val controller: AutocompleteUserController
) : RecyclerViewPresenter<User>(context), AutocompleteClickListener<User> {
var callback: Callback? = null
init {
controller.listener = this
}
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
// Also remove animation
recyclerView?.itemAnimator = null
return controller.adapter
}
override fun onItemClick(t: User) {
dispatchClick(t)
}
override fun onQuery(query: CharSequence?) {
callback?.onQueryUsers(query)
}
fun render(users: Async<List<User>>) {
if (users is Success) {
controller.setData(users())
}
}
interface Callback {
fun onQueryUsers(query: CharSequence?)
}
}

View file

@ -71,6 +71,14 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
.into(target) .into(target)
} }
@AnyThread
fun getCachedDrawable(glideRequest: GlideRequests, matrixItem: MatrixItem): Drawable {
return buildGlideRequest(glideRequest, matrixItem.avatarUrl)
.onlyRetrieveFromCache(true)
.submit()
.get()
}
@AnyThread @AnyThread
fun getPlaceholderDrawable(context: Context, matrixItem: MatrixItem): Drawable { fun getPlaceholderDrawable(context: Context, matrixItem: MatrixItem): Drawable {
val avatarColor = when (matrixItem) { val avatarColor = when (matrixItem) {

View file

@ -66,6 +66,11 @@ class HomeDetailFragment @Inject constructor(
setupToolbar() setupToolbar()
setupKeysBackupBanner() setupKeysBackupBanner()
withState(viewModel) {
// Update the navigation view if needed (for when we restore the tabs)
bottomNavigationView.selectedItemId = it.displayMode.toMenuId()
}
viewModel.selectSubscribe(this, HomeDetailViewState::groupSummary) { groupSummary -> viewModel.selectSubscribe(this, HomeDetailViewState::groupSummary) { groupSummary ->
onGroupChange(groupSummary.orNull()) onGroupChange(groupSummary.orNull())
} }
@ -127,7 +132,6 @@ class HomeDetailFragment @Inject constructor(
private fun setupBottomNavigationView() { private fun setupBottomNavigationView() {
bottomNavigationView.setOnNavigationItemSelectedListener { bottomNavigationView.setOnNavigationItemSelectedListener {
val displayMode = when (it.itemId) { val displayMode = when (it.itemId) {
R.id.bottom_action_home -> RoomListDisplayMode.HOME
R.id.bottom_action_people -> RoomListDisplayMode.PEOPLE R.id.bottom_action_people -> RoomListDisplayMode.PEOPLE
R.id.bottom_action_rooms -> RoomListDisplayMode.ROOMS R.id.bottom_action_rooms -> RoomListDisplayMode.ROOMS
else -> RoomListDisplayMode.HOME else -> RoomListDisplayMode.HOME
@ -149,12 +153,6 @@ class HomeDetailFragment @Inject constructor(
private fun switchDisplayMode(displayMode: RoomListDisplayMode) { private fun switchDisplayMode(displayMode: RoomListDisplayMode) {
groupToolbarTitleView.setText(displayMode.titleRes) groupToolbarTitleView.setText(displayMode.titleRes)
updateSelectedFragment(displayMode) updateSelectedFragment(displayMode)
// Update the navigation view (for when we restore the tabs)
bottomNavigationView.selectedItemId = when (displayMode) {
RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people
RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms
else -> R.id.bottom_action_home
}
} }
private fun updateSelectedFragment(displayMode: RoomListDisplayMode) { private fun updateSelectedFragment(displayMode: RoomListDisplayMode) {
@ -194,4 +192,10 @@ class HomeDetailFragment @Inject constructor(
unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms)) unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms))
syncStateView.render(it.syncState) syncStateView.render(it.syncState)
} }
private fun RoomListDisplayMode.toMenuId() = when (this) {
RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people
RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms
else -> R.id.bottom_action_home
}
} }

View file

@ -40,7 +40,7 @@ class HomeDrawerFragment @Inject constructor(
if (savedInstanceState == null) { if (savedInstanceState == null) {
replaceChildFragment(R.id.homeDrawerGroupListContainer, GroupListFragment::class.java) replaceChildFragment(R.id.homeDrawerGroupListContainer, GroupListFragment::class.java)
} }
session.liveUser(session.myUserId).observeK(this) { optionalUser -> session.getUserLive(session.myUserId).observeK(viewLifecycleOwner) { optionalUser ->
val user = optionalUser?.getOrNull() val user = optionalUser?.getOrNull()
if (user != null) { if (user != null) {
avatarRenderer.render(user.toMatrixItem(), homeDrawerHeaderAvatarView) avatarRenderer.render(user.toMatrixItem(), homeDrawerHeaderAvatarView)

View file

@ -24,7 +24,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
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 im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.groupSummaryQueryParams
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
@ -96,6 +98,10 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
} }
private fun observeGroupSummaries() { private fun observeGroupSummaries() {
val groupSummariesQueryParams = groupSummaryQueryParams {
memberships = listOf(Membership.JOIN)
displayName = QueryStringValue.IsNotEmpty
}
Observable.combineLatest<GroupSummary, List<GroupSummary>, List<GroupSummary>>( Observable.combineLatest<GroupSummary, List<GroupSummary>, List<GroupSummary>>(
session session
.rx() .rx()
@ -109,9 +115,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
}, },
session session
.rx() .rx()
.liveGroupSummaries() .liveGroupSummaries(groupSummariesQueryParams),
// Keep only joined groups. Group invitations will be managed later
.map { it.filter { groupSummary -> groupSummary.membership == Membership.JOIN } },
BiFunction { allCommunityGroup, communityGroups -> BiFunction { allCommunityGroup, communityGroups ->
listOf(allCommunityGroup) + communityGroups listOf(allCommunityGroup) + communityGroups
} }

View file

@ -24,9 +24,11 @@ import android.widget.EditText
import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy import com.otaliastudios.autocomplete.CharPolicy
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.api.util.toRoomAliasMatrixItem import im.vector.matrix.android.api.util.toRoomAliasMatrixItem
@ -36,24 +38,29 @@ import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresente
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotx.features.autocomplete.emoji.AutocompleteEmojiPresenter import im.vector.riotx.features.autocomplete.emoji.AutocompleteEmojiPresenter
import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter
import im.vector.riotx.features.autocomplete.member.AutocompleteMemberPresenter
import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
import im.vector.riotx.features.command.Command import im.vector.riotx.features.command.Command
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.html.PillImageSpan
import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.themes.ThemeUtils
import javax.inject.Inject
class AutoCompleter @Inject constructor( class AutoCompleter @AssistedInject constructor(
@Assisted val roomId: String,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val commandAutocompletePolicy: CommandAutocompletePolicy, private val commandAutocompletePolicy: CommandAutocompletePolicy,
private val autocompleteCommandPresenter: AutocompleteCommandPresenter, private val autocompleteCommandPresenter: AutocompleteCommandPresenter,
private val autocompleteUserPresenter: AutocompleteUserPresenter, private val autocompleteMemberPresenterFactory: AutocompleteMemberPresenter.Factory,
private val autocompleteRoomPresenter: AutocompleteRoomPresenter, private val autocompleteRoomPresenter: AutocompleteRoomPresenter,
private val autocompleteGroupPresenter: AutocompleteGroupPresenter, private val autocompleteGroupPresenter: AutocompleteGroupPresenter,
private val autocompleteEmojiPresenter: AutocompleteEmojiPresenter private val autocompleteEmojiPresenter: AutocompleteEmojiPresenter
) { ) {
@AssistedInject.Factory
interface Factory {
fun create(roomId: String): AutoCompleter
}
private lateinit var editText: EditText private lateinit var editText: EditText
fun enterSpecialMode() { fun enterSpecialMode() {
@ -68,22 +75,14 @@ class AutoCompleter @Inject constructor(
GlideApp.with(editText) GlideApp.with(editText)
} }
fun setup(editText: EditText, listener: AutoCompleterListener) { fun setup(editText: EditText) {
this.editText = editText this.editText = editText
val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(editText.context, R.attr.riotx_background)) val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(editText.context, R.attr.riotx_background))
setupCommands(backgroundDrawable, editText) setupCommands(backgroundDrawable, editText)
setupUsers(backgroundDrawable, editText, listener) setupMembers(backgroundDrawable, editText)
setupRooms(backgroundDrawable, editText, listener) setupGroups(backgroundDrawable, editText)
setupGroups(backgroundDrawable, editText, listener)
setupEmojis(backgroundDrawable, editText) setupEmojis(backgroundDrawable, editText)
} setupRooms(backgroundDrawable, editText)
fun render(state: TextComposerViewState) {
autocompleteUserPresenter.render(state.asyncUsers)
autocompleteRoomPresenter.render(state.asyncRooms)
autocompleteGroupPresenter.render(state.asyncGroups)
} }
private fun setupCommands(backgroundDrawable: Drawable, editText: EditText) { private fun setupCommands(backgroundDrawable: Drawable, editText: EditText) {
@ -107,15 +106,15 @@ class AutoCompleter @Inject constructor(
.build() .build()
} }
private fun setupUsers(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteUserPresenter.Callback) { private fun setupMembers(backgroundDrawable: ColorDrawable, editText: EditText) {
autocompleteUserPresenter.callback = listener val autocompleteMemberPresenter = autocompleteMemberPresenterFactory.create(roomId)
Autocomplete.on<User>(editText) Autocomplete.on<RoomMember>(editText)
.with(CharPolicy('@', true)) .with(CharPolicy('@', true))
.with(autocompleteUserPresenter) .with(autocompleteMemberPresenter)
.with(ELEVATION) .with(ELEVATION)
.with(backgroundDrawable) .with(backgroundDrawable)
.with(object : AutocompleteCallback<User> { .with(object : AutocompleteCallback<RoomMember> {
override fun onPopupItemClicked(editable: Editable, item: User): Boolean { override fun onPopupItemClicked(editable: Editable, item: RoomMember): Boolean {
insertMatrixItem(editText, editable, "@", item.toMatrixItem()) insertMatrixItem(editText, editable, "@", item.toMatrixItem())
return true return true
} }
@ -126,8 +125,7 @@ class AutoCompleter @Inject constructor(
.build() .build()
} }
private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteRoomPresenter.Callback) { private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText) {
autocompleteRoomPresenter.callback = listener
Autocomplete.on<RoomSummary>(editText) Autocomplete.on<RoomSummary>(editText)
.with(CharPolicy('#', true)) .with(CharPolicy('#', true))
.with(autocompleteRoomPresenter) .with(autocompleteRoomPresenter)
@ -145,8 +143,7 @@ class AutoCompleter @Inject constructor(
.build() .build()
} }
private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteGroupPresenter.Callback) { private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText) {
autocompleteGroupPresenter.callback = listener
Autocomplete.on<GroupSummary>(editText) Autocomplete.on<GroupSummary>(editText)
.with(CharPolicy('+', true)) .with(CharPolicy('+', true))
.with(autocompleteGroupPresenter) .with(autocompleteGroupPresenter)
@ -226,11 +223,6 @@ class AutoCompleter @Inject constructor(
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
interface AutoCompleterListener :
AutocompleteUserPresenter.Callback,
AutocompleteRoomPresenter.Callback,
AutocompleteGroupPresenter.Callback
companion object { companion object {
private const val ELEVATION = 6f private const val ELEVATION = 6f
} }

View file

@ -78,10 +78,7 @@ import im.vector.riotx.features.attachments.ContactAttachment
import im.vector.riotx.features.command.Command import im.vector.riotx.features.command.Command
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.getColorFromUserId import im.vector.riotx.features.home.getColorFromUserId
import im.vector.riotx.features.home.room.detail.composer.TextComposerAction
import im.vector.riotx.features.home.room.detail.composer.TextComposerView import im.vector.riotx.features.home.room.detail.composer.TextComposerView
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.action.EventSharedAction import im.vector.riotx.features.home.room.detail.timeline.action.EventSharedAction
@ -127,17 +124,15 @@ class RoomDetailFragment @Inject constructor(
private val session: Session, private val session: Session,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val timelineEventController: TimelineEventController, private val timelineEventController: TimelineEventController,
private val autoCompleter: AutoCompleter, autoCompleterFactory: AutoCompleter.Factory,
private val permalinkHandler: PermalinkHandler, private val permalinkHandler: PermalinkHandler,
private val notificationDrawerManager: NotificationDrawerManager, private val notificationDrawerManager: NotificationDrawerManager,
val roomDetailViewModelFactory: RoomDetailViewModel.Factory, val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
val textComposerViewModelFactory: TextComposerViewModel.Factory,
private val eventHtmlRenderer: EventHtmlRenderer, private val eventHtmlRenderer: EventHtmlRenderer,
private val vectorPreferences: VectorPreferences private val vectorPreferences: VectorPreferences
) : ) :
VectorBaseFragment(), VectorBaseFragment(),
TimelineEventController.Callback, TimelineEventController.Callback,
AutoCompleter.AutoCompleterListener,
VectorInviteView.Callback, VectorInviteView.Callback,
JumpToReadMarkerView.Callback, JumpToReadMarkerView.Callback,
AttachmentTypeSelectorView.Callback, AttachmentTypeSelectorView.Callback,
@ -167,9 +162,10 @@ class RoomDetailFragment @Inject constructor(
GlideApp.with(this) GlideApp.with(this)
} }
private val autoCompleter: AutoCompleter by lazy {
autoCompleterFactory.create(roomDetailArgs.roomId)
}
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
private val textComposerViewModel: TextComposerViewModel by fragmentViewModel()
private val debouncer = Debouncer(createUIHandler()) private val debouncer = Debouncer(createUIHandler())
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
@ -205,9 +201,9 @@ class RoomDetailFragment @Inject constructor(
setupNotificationView() setupNotificationView()
setupJumpToReadMarkerView() setupJumpToReadMarkerView()
setupJumpToBottomView() setupJumpToBottomView()
roomDetailViewModel.subscribe { renderState(it) } roomDetailViewModel.subscribe { renderState(it) }
textComposerViewModel.subscribe { renderTextComposerState(it) } roomDetailViewModel.sendMessageResultLiveData.observeEvent(viewLifecycleOwner) { renderSendMessageResult(it) }
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
roomDetailViewModel.nonBlockingPopAlert.observeEvent(this) { pair -> roomDetailViewModel.nonBlockingPopAlert.observeEvent(this) { pair ->
val message = requireContext().getString(pair.first, *pair.second.toTypedArray()) val message = requireContext().getString(pair.first, *pair.second.toTypedArray())
@ -250,9 +246,9 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode) { mode -> roomDetailViewModel.selectSubscribe(RoomDetailViewState::sendMode) { mode ->
when (mode) { when (mode) {
is SendMode.REGULAR -> renderRegularMode(mode.text) is SendMode.REGULAR -> renderRegularMode(mode.text)
is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text) is SendMode.EDIT -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_edit, R.string.edit, mode.text)
is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text) is SendMode.QUOTE -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_quote, R.string.quote, mode.text)
is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text) is SendMode.REPLY -> renderSpecialMode(mode.timelineEvent, R.drawable.ic_reply, R.string.reply, mode.text)
} }
} }
@ -279,9 +275,9 @@ class RoomDetailFragment @Inject constructor(
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
if (savedInstanceState == null) { if (savedInstanceState == null) {
when (val sharedData = roomDetailArgs.sharedData) { when (val sharedData = roomDetailArgs.sharedData) {
is SharedData.Text -> roomDetailViewModel.handle(RoomDetailAction.SendMessage(sharedData.text, false)) is SharedData.Text -> roomDetailViewModel.handle(RoomDetailAction.SendMessage(sharedData.text, false))
is SharedData.Attachments -> roomDetailViewModel.handle(RoomDetailAction.SendMedia(sharedData.attachmentData)) is SharedData.Attachments -> roomDetailViewModel.handle(RoomDetailAction.SendMedia(sharedData.attachmentData))
null -> Timber.v("No share data to process") null -> Timber.v("No share data to process")
} }
} }
} }
@ -305,12 +301,10 @@ class RoomDetailFragment @Inject constructor(
jumpToBottomView.setOnClickListener { jumpToBottomView.setOnClickListener {
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
jumpToBottomView.visibility = View.INVISIBLE jumpToBottomView.visibility = View.INVISIBLE
withState(roomDetailViewModel) { state -> if (!roomDetailViewModel.timeline.isLive) {
if (state.timeline?.isLive == false) { roomDetailViewModel.timeline.restartWithEventId(null)
state.timeline.restartWithEventId(null) } else {
} else { layoutManager.scrollToPosition(0)
layoutManager.scrollToPosition(0)
}
} }
} }
@ -418,7 +412,8 @@ class RoomDetailFragment @Inject constructor(
composerLayout.sendButton.setContentDescription(getString(descriptionRes)) composerLayout.sendButton.setContentDescription(getString(descriptionRes))
avatarRenderer.render( avatarRenderer.render(
MatrixItem.UserItem(event.root.senderId ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar), MatrixItem.UserItem(event.root.senderId
?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
composerLayout.composerRelatedMessageAvatar composerLayout.composerRelatedMessageAvatar
) )
composerLayout.expand { composerLayout.expand {
@ -468,6 +463,9 @@ class RoomDetailFragment @Inject constructor(
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************
private fun setupRecyclerView() { private fun setupRecyclerView() {
timelineEventController.callback = this
timelineEventController.timeline = roomDetailViewModel.timeline
val epoxyVisibilityTracker = EpoxyVisibilityTracker() val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(recyclerView) epoxyVisibilityTracker.attach(recyclerView)
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
@ -487,8 +485,6 @@ class RoomDetailFragment @Inject constructor(
timelineEventController.addModelBuildListener(modelBuildListener) timelineEventController.addModelBuildListener(modelBuildListener)
recyclerView.adapter = timelineEventController.adapter recyclerView.adapter = timelineEventController.adapter
timelineEventController.callback = this
if (vectorPreferences.swipeToReplyIsEnabled()) { if (vectorPreferences.swipeToReplyIsEnabled()) {
val quickReplyHandler = object : RoomMessageTouchHelperCallback.QuickReplayHandler { val quickReplyHandler = object : RoomMessageTouchHelperCallback.QuickReplayHandler {
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
@ -505,7 +501,7 @@ class RoomDetailFragment @Inject constructor(
is MessageTextItem -> { is MessageTextItem -> {
return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED
} }
else -> false else -> false
} }
} }
} }
@ -520,9 +516,9 @@ class RoomDetailFragment @Inject constructor(
withState(roomDetailViewModel) { withState(roomDetailViewModel) {
val showJumpToUnreadBanner = when (it.unreadState) { val showJumpToUnreadBanner = when (it.unreadState) {
UnreadState.Unknown, UnreadState.Unknown,
UnreadState.HasNoUnread -> false UnreadState.HasNoUnread -> false
is UnreadState.ReadMarkerNotLoaded -> true is UnreadState.ReadMarkerNotLoaded -> true
is UnreadState.HasUnread -> { is UnreadState.HasUnread -> {
if (it.canShowJumpToReadMarker) { if (it.canShowJumpToReadMarker) {
val lastVisibleItem = layoutManager.findLastVisibleItemPosition() val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
val positionOfReadMarker = timelineEventController.getPositionOfReadMarker() val positionOfReadMarker = timelineEventController.getPositionOfReadMarker()
@ -536,14 +532,13 @@ class RoomDetailFragment @Inject constructor(
} }
} }
} }
jumpToReadMarkerView.isVisible = showJumpToUnreadBanner jumpToReadMarkerView?.isVisible = showJumpToUnreadBanner
} }
} }
} }
private fun setupComposer() { private fun setupComposer() {
autoCompleter.setup(composerLayout.composerEditText, this) autoCompleter.setup(composerLayout.composerEditText)
composerLayout.callback = object : TextComposerView.Callback { composerLayout.callback = object : TextComposerView.Callback {
override fun onAddAttachment() { override fun onAddAttachment() {
if (!::attachmentTypeSelector.isInitialized) { if (!::attachmentTypeSelector.isInitialized) {
@ -598,7 +593,7 @@ class RoomDetailFragment @Inject constructor(
val summary = state.asyncRoomSummary() val summary = state.asyncRoomSummary()
val inviter = state.asyncInviter() val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) { if (summary?.membership == Membership.JOIN) {
scrollOnHighlightedEventCallback.timeline = state.timeline scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
timelineEventController.update(state) timelineEventController.update(state)
inviteView.visibility = View.GONE inviteView.visibility = View.GONE
val uid = session.myUserId val uid = session.myUserId
@ -613,9 +608,10 @@ class RoomDetailFragment @Inject constructor(
} else if (state.asyncInviter.complete) { } else if (state.asyncInviter.complete) {
vectorBaseActivity.finish() vectorBaseActivity.finish()
} }
val isRoomEncrypted = summary?.isEncrypted ?: false
if (state.tombstoneEvent == null) { if (state.tombstoneEvent == null) {
composerLayout.visibility = View.VISIBLE composerLayout.visibility = View.VISIBLE
composerLayout.setRoomEncrypted(state.isEncrypted) composerLayout.setRoomEncrypted(isRoomEncrypted)
notificationAreaView.render(NotificationAreaView.State.Hidden) notificationAreaView.render(NotificationAreaView.State.Hidden)
} else { } else {
composerLayout.visibility = View.GONE composerLayout.visibility = View.GONE
@ -638,10 +634,6 @@ class RoomDetailFragment @Inject constructor(
} }
} }
private fun renderTextComposerState(state: TextComposerViewState) {
autoCompleter.render(state)
}
private fun renderTombstoneEventHandling(async: Async<String>) { private fun renderTombstoneEventHandling(async: Async<String>) {
when (async) { when (async) {
is Loading -> { is Loading -> {
@ -654,7 +646,7 @@ class RoomDetailFragment @Inject constructor(
navigator.openRoom(vectorBaseActivity, async()) navigator.openRoom(vectorBaseActivity, async())
vectorBaseActivity.finish() vectorBaseActivity.finish()
} }
is Fail -> { is Fail -> {
vectorBaseActivity.hideWaitingView() vectorBaseActivity.hideWaitingView()
vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error)) vectorBaseActivity.toast(errorFormatter.toHumanReadable(async.error))
} }
@ -663,23 +655,23 @@ class RoomDetailFragment @Inject constructor(
private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { private fun renderSendMessageResult(sendMessageResult: SendMessageResult) {
when (sendMessageResult) { when (sendMessageResult) {
is SendMessageResult.MessageSent -> { is SendMessageResult.MessageSent -> {
updateComposerText("") updateComposerText("")
} }
is SendMessageResult.SlashCommandHandled -> { is SendMessageResult.SlashCommandHandled -> {
sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) }
updateComposerText("") updateComposerText("")
} }
is SendMessageResult.SlashCommandError -> { is SendMessageResult.SlashCommandError -> {
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
} }
is SendMessageResult.SlashCommandUnknown -> { is SendMessageResult.SlashCommandUnknown -> {
displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command))
} }
is SendMessageResult.SlashCommandResultOk -> { is SendMessageResult.SlashCommandResultOk -> {
updateComposerText("") updateComposerText("")
} }
is SendMessageResult.SlashCommandResultError -> { is SendMessageResult.SlashCommandResultError -> {
displayCommandError(sendMessageResult.throwable.localizedMessage) displayCommandError(sendMessageResult.throwable.localizedMessage)
} }
is SendMessageResult.SlashCommandNotImplemented -> { is SendMessageResult.SlashCommandNotImplemented -> {
@ -717,7 +709,7 @@ class RoomDetailFragment @Inject constructor(
private fun displayRoomDetailActionResult(result: Async<RoomDetailAction>) { private fun displayRoomDetailActionResult(result: Async<RoomDetailAction>) {
when (result) { when (result) {
is Fail -> { is Fail -> {
AlertDialog.Builder(requireActivity()) AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error) .setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(result.error)) .setMessage(errorFormatter.toHumanReadable(result.error))
@ -728,7 +720,7 @@ class RoomDetailFragment @Inject constructor(
when (val data = result.invoke()) { when (val data = result.invoke()) {
is RoomDetailAction.ReportContent -> { is RoomDetailAction.ReportContent -> {
when { when {
data.spam -> { data.spam -> {
AlertDialog.Builder(requireActivity()) AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_as_spam_title) .setTitle(R.string.content_reported_as_spam_title)
.setMessage(R.string.content_reported_as_spam_content) .setMessage(R.string.content_reported_as_spam_content)
@ -750,7 +742,7 @@ class RoomDetailFragment @Inject constructor(
.show() .show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE) .withColoredButton(DialogInterface.BUTTON_NEGATIVE)
} }
else -> { else -> {
AlertDialog.Builder(requireActivity()) AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_title) .setTitle(R.string.content_reported_title)
.setMessage(R.string.content_reported_content) .setMessage(R.string.content_reported_content)
@ -863,14 +855,14 @@ class RoomDetailFragment @Inject constructor(
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
if (allGranted(grantResults)) { if (allGranted(grantResults)) {
when (requestCode) { when (requestCode) {
PERMISSION_REQUEST_CODE_DOWNLOAD_FILE -> { PERMISSION_REQUEST_CODE_DOWNLOAD_FILE -> {
val action = roomDetailViewModel.pendingAction val action = roomDetailViewModel.pendingAction
if (action != null) { if (action != null) {
roomDetailViewModel.pendingAction = null roomDetailViewModel.pendingAction = null
roomDetailViewModel.handle(action) roomDetailViewModel.handle(action)
} }
} }
PERMISSION_REQUEST_CODE_INCOMING_URI -> { PERMISSION_REQUEST_CODE_INCOMING_URI -> {
val pendingUri = roomDetailViewModel.pendingUri val pendingUri = roomDetailViewModel.pendingUri
if (pendingUri != null) { if (pendingUri != null) {
roomDetailViewModel.pendingUri = null roomDetailViewModel.pendingUri = null
@ -966,43 +958,25 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState) roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
} }
// AutocompleteUserPresenter.Callback
override fun onQueryUsers(query: CharSequence?) {
textComposerViewModel.handle(TextComposerAction.QueryUsers(query))
}
// AutocompleteRoomPresenter.Callback
override fun onQueryRooms(query: CharSequence?) {
textComposerViewModel.handle(TextComposerAction.QueryRooms(query))
}
// AutocompleteGroupPresenter.Callback
override fun onQueryGroups(query: CharSequence?) {
textComposerViewModel.handle(TextComposerAction.QueryGroups(query))
}
private fun handleActions(action: EventSharedAction) { private fun handleActions(action: EventSharedAction) {
when (action) { when (action) {
is EventSharedAction.AddReaction -> { is EventSharedAction.AddReaction -> {
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE) startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE)
} }
is EventSharedAction.ViewReactions -> { is EventSharedAction.ViewReactions -> {
ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData) ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
} }
is EventSharedAction.Copy -> { is EventSharedAction.Copy -> {
// I need info about the current selected message :/ // I need info about the current selected message :/
copyToClipboard(requireContext(), action.content, false) copyToClipboard(requireContext(), action.content, false)
val msg = requireContext().getString(R.string.copied_to_clipboard) val msg = requireContext().getString(R.string.copied_to_clipboard)
showSnackWithMessage(msg, Snackbar.LENGTH_SHORT) showSnackWithMessage(msg, Snackbar.LENGTH_SHORT)
} }
is EventSharedAction.Delete -> { is EventSharedAction.Delete -> {
roomDetailViewModel.handle(RoomDetailAction.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason))) roomDetailViewModel.handle(RoomDetailAction.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason)))
} }
is EventSharedAction.Share -> { is EventSharedAction.Share -> {
// TODO current data communication is too limited // TODO current data communication is too limited
// Need to now the media type // Need to now the media type
// TODO bad, just POC // TODO bad, just POC
@ -1030,10 +1004,10 @@ class RoomDetailFragment @Inject constructor(
} }
) )
} }
is EventSharedAction.ViewEditHistory -> { is EventSharedAction.ViewEditHistory -> {
onEditedDecorationClicked(action.messageInformationData) onEditedDecorationClicked(action.messageInformationData)
} }
is EventSharedAction.ViewSource -> { is EventSharedAction.ViewSource -> {
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
view.findViewById<TextView>(R.id.event_content_text_view)?.let { view.findViewById<TextView>(R.id.event_content_text_view)?.let {
it.text = action.content it.text = action.content
@ -1044,7 +1018,7 @@ class RoomDetailFragment @Inject constructor(
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} }
is EventSharedAction.ViewDecryptedSource -> { is EventSharedAction.ViewDecryptedSource -> {
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
view.findViewById<TextView>(R.id.event_content_text_view)?.let { view.findViewById<TextView>(R.id.event_content_text_view)?.let {
it.text = action.content it.text = action.content
@ -1055,31 +1029,31 @@ class RoomDetailFragment @Inject constructor(
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} }
is EventSharedAction.QuickReact -> { is EventSharedAction.QuickReact -> {
// eventId,ClickedOn,Add // eventId,ClickedOn,Add
roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) roomDetailViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
} }
is EventSharedAction.Edit -> { is EventSharedAction.Edit -> {
roomDetailViewModel.handle(RoomDetailAction.EnterEditMode(action.eventId, composerLayout.text.toString())) roomDetailViewModel.handle(RoomDetailAction.EnterEditMode(action.eventId, composerLayout.text.toString()))
} }
is EventSharedAction.Quote -> { is EventSharedAction.Quote -> {
roomDetailViewModel.handle(RoomDetailAction.EnterQuoteMode(action.eventId, composerLayout.text.toString())) roomDetailViewModel.handle(RoomDetailAction.EnterQuoteMode(action.eventId, composerLayout.text.toString()))
} }
is EventSharedAction.Reply -> { is EventSharedAction.Reply -> {
roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(action.eventId, composerLayout.text.toString())) roomDetailViewModel.handle(RoomDetailAction.EnterReplyMode(action.eventId, composerLayout.text.toString()))
} }
is EventSharedAction.CopyPermalink -> { is EventSharedAction.CopyPermalink -> {
val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId) val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId)
copyToClipboard(requireContext(), permalink, false) copyToClipboard(requireContext(), permalink, false)
showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
} }
is EventSharedAction.Resend -> { is EventSharedAction.Resend -> {
roomDetailViewModel.handle(RoomDetailAction.ResendMessage(action.eventId)) roomDetailViewModel.handle(RoomDetailAction.ResendMessage(action.eventId))
} }
is EventSharedAction.Remove -> { is EventSharedAction.Remove -> {
roomDetailViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId)) roomDetailViewModel.handle(RoomDetailAction.RemoveFailedEcho(action.eventId))
} }
is EventSharedAction.ReportContentSpam -> { is EventSharedAction.ReportContentSpam -> {
roomDetailViewModel.handle(RoomDetailAction.ReportContent( roomDetailViewModel.handle(RoomDetailAction.ReportContent(
action.eventId, action.senderId, "This message is spam", spam = true)) action.eventId, action.senderId, "This message is spam", spam = true))
} }
@ -1087,19 +1061,19 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.ReportContent( roomDetailViewModel.handle(RoomDetailAction.ReportContent(
action.eventId, action.senderId, "This message is inappropriate", inappropriate = true)) action.eventId, action.senderId, "This message is inappropriate", inappropriate = true))
} }
is EventSharedAction.ReportContentCustom -> { is EventSharedAction.ReportContentCustom -> {
promptReasonToReportContent(action) promptReasonToReportContent(action)
} }
is EventSharedAction.IgnoreUser -> { is EventSharedAction.IgnoreUser -> {
roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(action.senderId)) roomDetailViewModel.handle(RoomDetailAction.IgnoreUser(action.senderId))
} }
is EventSharedAction.OnUrlClicked -> { is EventSharedAction.OnUrlClicked -> {
onUrlClicked(action.url) onUrlClicked(action.url)
} }
is EventSharedAction.OnUrlLongClicked -> { is EventSharedAction.OnUrlLongClicked -> {
onUrlLongClicked(action.url) onUrlLongClicked(action.url)
} }
else -> { else -> {
Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show() Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show()
} }
} }
@ -1207,10 +1181,10 @@ class RoomDetailFragment @Inject constructor(
private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) { private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
when (type) { when (type) {
AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera() AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera()
AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile() AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile()
AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery() AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery()
AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio() AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio()
AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact() AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact()
AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers") AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers")
} }

View file

@ -20,14 +20,18 @@ import android.net.Uri
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.* import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay import com.jakewharton.rxrelay2.BehaviorRelay
import com.jakewharton.rxrelay2.PublishRelay import com.jakewharton.rxrelay2.PublishRelay
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 im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
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.isImageMessage import im.vector.matrix.android.api.session.events.model.isImageMessage
@ -89,20 +93,21 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private val visibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsVisible>() private val visibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsVisible>()
private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) { private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) {
TimelineSettings(30, TimelineSettings(30,
filterEdits = false, filterEdits = false,
filterTypes = true, filterTypes = true,
allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES, allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES,
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
} else { } else {
TimelineSettings(30, TimelineSettings(30,
filterEdits = true, filterEdits = true,
filterTypes = true, filterTypes = true,
allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES, allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES,
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
} }
private var timelineEvents = PublishRelay.create<List<TimelineEvent>>() private var timelineEvents = PublishRelay.create<List<TimelineEvent>>()
private var timeline = room.createTimeline(eventId, timelineSettings) var timeline = room.createTimeline(eventId, timelineSettings)
private set
private val _viewEvents = PublishDataSource<RoomDetailViewEvents>() private val _viewEvents = PublishDataSource<RoomDetailViewEvents>()
val viewEvents: DataSource<RoomDetailViewEvents> = _viewEvents val viewEvents: DataSource<RoomDetailViewEvents> = _viewEvents
@ -138,18 +143,17 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
init { init {
timeline.start()
timeline.addListener(this)
observeRoomSummary()
observeSummaryState()
getUnreadState() getUnreadState()
observeSyncState() observeSyncState()
observeRoomSummary()
observeEventDisplayedActions() observeEventDisplayedActions()
observeSummaryState()
observeDrafts() observeDrafts()
observeUnreadState() observeUnreadState()
room.getRoomSummaryLive()
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
timeline.addListener(this)
timeline.start()
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
// Inform the SDK that the room is displayed // Inform the SDK that the room is displayed
session.onRoomDisplayed(initialState.roomId) session.onRoomDisplayed(initialState.roomId)
} }
@ -233,23 +237,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
copy( copy(
// Create a sendMode from a draft and retrieve the TimelineEvent // Create a sendMode from a draft and retrieve the TimelineEvent
sendMode = when (draft) { sendMode = when (draft) {
is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) is UserDraft.REGULAR -> SendMode.REGULAR(draft.text)
is UserDraft.QUOTE -> { is UserDraft.QUOTE -> {
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
SendMode.QUOTE(timelineEvent, draft.text) SendMode.QUOTE(timelineEvent, draft.text)
} }
} }
is UserDraft.REPLY -> { is UserDraft.REPLY -> {
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
SendMode.REPLY(timelineEvent, draft.text) SendMode.REPLY(timelineEvent, draft.text)
} }
} }
is UserDraft.EDIT -> { is UserDraft.EDIT -> {
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
SendMode.EDIT(timelineEvent, draft.text) SendMode.EDIT(timelineEvent, draft.text)
} }
} }
} ?: SendMode.REGULAR("") } ?: SendMode.REGULAR("")
) )
} }
} }
@ -258,7 +262,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) { private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) {
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>() val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>()
?: return ?: return
val roomId = tombstoneContent.replacementRoom ?: "" val roomId = tombstoneContent.replacementRoom ?: ""
val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN
@ -310,7 +314,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
else -> false else -> false
} }
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************
private fun handleSendMessage(action: RoomDetailAction.SendMessage) { private fun handleSendMessage(action: RoomDetailAction.SendMessage) {
withState { state -> withState { state ->
@ -396,7 +400,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is SendMode.EDIT -> { is SendMode.EDIT -> {
// is original event a reply? // is original event a reply?
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId ?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
if (inReplyTo != null) { if (inReplyTo != null) {
// TODO check if same content? // TODO check if same content?
room.getTimeLineEvent(inReplyTo)?.let { room.getTimeLineEvent(inReplyTo)?.let {
@ -405,13 +409,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} else { } else {
val messageContent: MessageContent? = val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel() ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
val existingBody = messageContent?.body ?: "" val existingBody = messageContent?.body ?: ""
if (existingBody != action.text) { if (existingBody != action.text) {
room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "", room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "",
messageContent?.type ?: MessageType.MSGTYPE_TEXT, messageContent?.type ?: MessageType.MSGTYPE_TEXT,
action.text, action.text,
action.autoMarkdown) action.autoMarkdown)
} else { } else {
Timber.w("Same message content, do not send edition") Timber.w("Same message content, do not send edition")
} }
@ -422,7 +426,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is SendMode.QUOTE -> { is SendMode.QUOTE -> {
val messageContent: MessageContent? = val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel() ?: state.sendMode.timelineEvent.root.getClearContent().toModel()
val textMsg = messageContent?.body val textMsg = messageContent?.body
val finalText = legacyRiotQuoteText(textMsg, action.text.toString()) val finalText = legacyRiotQuoteText(textMsg, action.text.toString())
@ -538,7 +542,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) { when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
null -> room.sendMedias(attachments) null -> room.sendMedias(attachments)
else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name
?: tooBigFile.path, tooBigFile.size, maxUploadFileSize))) ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
} }
} }
} }
@ -728,7 +732,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
.subscribeBy(onNext = { actions -> .subscribeBy(onNext = { actions ->
val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event
?: return@subscribeBy ?: return@subscribeBy
val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent
if (trackUnreadMessages.get()) { if (trackUnreadMessages.get()) {
if (globalMostRecentDisplayedEvent == null) { if (globalMostRecentDisplayedEvent == null) {
@ -791,10 +795,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
room.rx().liveRoomSummary() room.rx().liveRoomSummary()
.unwrap() .unwrap()
.execute { async -> .execute { async ->
copy( copy(asyncRoomSummary = async)
asyncRoomSummary = async,
isEncrypted = room.isEncrypted()
)
} }
} }
@ -880,7 +881,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
override fun onCleared() { override fun onCleared() {
timeline.dispose() timeline.dispose()
timeline.removeListener(this) timeline.removeAllListeners()
super.onCleared() super.onCleared()
} }
} }

View file

@ -21,7 +21,6 @@ import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
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.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
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
import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
@ -51,11 +50,9 @@ sealed class UnreadState {
data class RoomDetailViewState( data class RoomDetailViewState(
val roomId: String, val roomId: String,
val eventId: String?, val eventId: String?,
val timeline: Timeline? = null,
val asyncInviter: Async<User> = Uninitialized, val asyncInviter: Async<User> = Uninitialized,
val asyncRoomSummary: Async<RoomSummary> = Uninitialized, val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
val sendMode: SendMode = SendMode.REGULAR(""), val sendMode: SendMode = SendMode.REGULAR(""),
val isEncrypted: Boolean = false,
val tombstoneEvent: Event? = null, val tombstoneEvent: Event? = null,
val tombstoneEventHandling: Async<String> = Uninitialized, val tombstoneEventHandling: Async<String> = Uninitialized,
val syncState: SyncState = SyncState.Idle, val syncState: SyncState = SyncState.Idle,

View file

@ -1,158 +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.riotx.features.home.room.detail.composer
import arrow.core.Option
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.home.room.detail.RoomDetailFragment
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import java.util.concurrent.TimeUnit
typealias AutocompleteQuery = CharSequence
class TextComposerViewModel @AssistedInject constructor(@Assisted initialState: TextComposerViewState,
private val session: Session
) : VectorViewModel<TextComposerViewState, TextComposerAction>(initialState) {
private val room = session.getRoom(initialState.roomId)!!
private val usersQueryObservable = BehaviorRelay.create<Option<AutocompleteQuery>>()
private val roomsQueryObservable = BehaviorRelay.create<Option<AutocompleteQuery>>()
private val groupsQueryObservable = BehaviorRelay.create<Option<AutocompleteQuery>>()
@AssistedInject.Factory
interface Factory {
fun create(initialState: TextComposerViewState): TextComposerViewModel
}
companion object : MvRxViewModelFactory<TextComposerViewModel, TextComposerViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: TextComposerViewState): TextComposerViewModel? {
val fragment: RoomDetailFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.textComposerViewModelFactory.create(state)
}
}
init {
observeUsersQuery()
observeRoomsQuery()
observeGroupsQuery()
}
override fun handle(action: TextComposerAction) {
when (action) {
is TextComposerAction.QueryUsers -> handleQueryUsers(action)
is TextComposerAction.QueryRooms -> handleQueryRooms(action)
is TextComposerAction.QueryGroups -> handleQueryGroups(action)
}
}
private fun handleQueryUsers(action: TextComposerAction.QueryUsers) {
val query = Option.fromNullable(action.query)
usersQueryObservable.accept(query)
}
private fun handleQueryRooms(action: TextComposerAction.QueryRooms) {
val query = Option.fromNullable(action.query)
roomsQueryObservable.accept(query)
}
private fun handleQueryGroups(action: TextComposerAction.QueryGroups) {
val query = Option.fromNullable(action.query)
groupsQueryObservable.accept(query)
}
private fun observeUsersQuery() {
Observable.combineLatest<List<String>, Option<AutocompleteQuery>, List<User>>(
room.rx().liveRoomMemberIds(),
usersQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS),
BiFunction { roomMemberIds, query ->
val users = roomMemberIds.mapNotNull { session.getUser(it) }
val filter = query.orNull()
if (filter.isNullOrBlank()) {
users
} else {
users.filter {
it.displayName?.contains(filter, ignoreCase = true) ?: false
}
}
.sortedBy { it.displayName }
}
).execute { async ->
copy(
asyncUsers = async
)
}
}
private fun observeRoomsQuery() {
Observable.combineLatest<List<RoomSummary>, Option<AutocompleteQuery>, List<RoomSummary>>(
session.rx().liveRoomSummaries(),
roomsQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS),
BiFunction { roomSummaries, query ->
val filter = query.orNull() ?: ""
// Keep only room with a canonical alias
roomSummaries
.filter {
it.canonicalAlias?.contains(filter, ignoreCase = true) == true
}
.sortedBy { it.displayName }
}
).execute { async ->
copy(
asyncRooms = async
)
}
}
private fun observeGroupsQuery() {
Observable.combineLatest<List<GroupSummary>, Option<AutocompleteQuery>, List<GroupSummary>>(
session.rx().liveGroupSummaries(),
groupsQueryObservable.throttleLast(300, TimeUnit.MILLISECONDS),
BiFunction { groupSummaries, query ->
val filter = query.orNull()
if (filter.isNullOrBlank()) {
groupSummaries
} else {
groupSummaries
.filter {
it.groupId.contains(filter, ignoreCase = true)
}
}
.sortedBy { it.displayName }
}
).execute { async ->
copy(
asyncGroups = async
)
}
}
}

View file

@ -1,34 +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.riotx.features.home.room.detail.composer
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.features.home.room.detail.RoomDetailArgs
data class TextComposerViewState(val roomId: String,
val asyncUsers: Async<List<User>> = Uninitialized,
val asyncRooms: Async<List<RoomSummary>> = Uninitialized,
val asyncGroups: Async<List<GroupSummary>> = Uninitialized
) : MvRxState {
constructor(args: RoomDetailArgs) : this(roomId = args.roomId)
}

View file

@ -95,12 +95,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private val modelCache = arrayListOf<CacheItemData?>() private val modelCache = arrayListOf<CacheItemData?>()
private var currentSnapshot: List<TimelineEvent> = emptyList() private var currentSnapshot: List<TimelineEvent> = emptyList()
private var inSubmitList: Boolean = false private var inSubmitList: Boolean = false
private var timeline: Timeline? = null
private var unreadState: UnreadState = UnreadState.Unknown private var unreadState: UnreadState = UnreadState.Unknown
private var positionOfReadMarker: Int? = null private var positionOfReadMarker: Int? = null
private var eventIdToHighlight: String? = null private var eventIdToHighlight: String? = null
var callback: Callback? = null var callback: Callback? = null
var timeline: Timeline? = null
private val listUpdateCallback = object : ListUpdateCallback { private val listUpdateCallback = object : ListUpdateCallback {
@ -176,10 +176,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
} }
fun update(viewState: RoomDetailViewState) { fun update(viewState: RoomDetailViewState) {
if (timeline?.timelineID != viewState.timeline?.timelineID) {
timeline = viewState.timeline
timeline?.addListener(this)
}
var requestModelBuild = false var requestModelBuild = false
if (eventIdToHighlight != viewState.highlightedEventId) { if (eventIdToHighlight != viewState.highlightedEventId) {
// Clear cache to force a refresh // Clear cache to force a refresh
@ -205,6 +201,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView) super.onAttachedToRecyclerView(recyclerView)
timeline?.addListener(this)
timelineMediaSizeProvider.recyclerView = recyclerView timelineMediaSizeProvider.recyclerView = recyclerView
} }

View file

@ -93,7 +93,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid
} }
// Action // Action
state.actions()?.forEachIndexed { index, action -> state.actions.forEachIndexed { index, action ->
if (action is EventSharedAction.Separator) { if (action is EventSharedAction.Separator) {
bottomSheetSeparatorItem { bottomSheetSeparatorItem {
id("separator_$index") id("separator_$index")

View file

@ -31,8 +31,7 @@ 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.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.rx.rx
import im.vector.matrix.rx.RxRoom
import im.vector.matrix.rx.unwrap import im.vector.matrix.rx.unwrap
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.canReact import im.vector.riotx.core.extensions.canReact
@ -42,8 +41,8 @@ import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventForm
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.VectorHtmlCompressor import im.vector.riotx.features.html.VectorHtmlCompressor
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.reactions.data.EmojiDataSource import im.vector.riotx.features.reactions.data.EmojiDataSource
import im.vector.riotx.features.settings.VectorPreferences
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -64,7 +63,7 @@ data class MessageActionState(
// For quick reactions // For quick reactions
val quickStates: Async<List<ToggleState>> = Uninitialized, val quickStates: Async<List<ToggleState>> = Uninitialized,
// For actions // For actions
val actions: Async<List<EventSharedAction>> = Uninitialized, val actions: List<EventSharedAction> = emptyList(),
val expendedReportContentMenu: Boolean = false val expendedReportContentMenu: Boolean = false
) : MvRxState { ) : MvRxState {
@ -112,7 +111,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
init { init {
observeEvent() observeEvent()
observeReactions() observeReactions()
observeEventAction() observeTimelineEventState()
} }
override fun handle(action: MessageActionsAction) { override fun handle(action: MessageActionsAction) {
@ -131,32 +130,17 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private fun observeEvent() { private fun observeEvent() {
if (room == null) return if (room == null) return
RxRoom(room) room.rx()
.liveTimelineEvent(eventId) .liveTimelineEvent(eventId)
.unwrap() .unwrap()
.execute { .execute {
copy( copy(timelineEvent = it)
timelineEvent = it,
messageBody = computeMessageBody(it)
)
}
}
private fun observeEventAction() {
if (room == null) return
RxRoom(room)
.liveTimelineEvent(eventId)
.map {
actionsForEvent(it)
}
.execute {
copy(actions = it)
} }
} }
private fun observeReactions() { private fun observeReactions() {
if (room == null) return if (room == null) return
RxRoom(room) room.rx()
.liveAnnotationSummary(eventId) .liveAnnotationSummary(eventId)
.map { annotations -> .map { annotations ->
EmojiDataSource.quickEmojis.map { emoji -> EmojiDataSource.quickEmojis.map { emoji ->
@ -168,11 +152,19 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
} }
} }
private fun computeMessageBody(timelineEvent: Async<TimelineEvent>): CharSequence? { private fun observeTimelineEventState() {
return when (timelineEvent()?.root?.getClearType()) { asyncSubscribe(MessageActionState::timelineEvent) { timelineEvent ->
val computedMessage = computeMessageBody(timelineEvent)
val actions = actionsForEvent(timelineEvent)
setState { copy(messageBody = computedMessage, actions = actions) }
}
}
private fun computeMessageBody(timelineEvent: TimelineEvent): CharSequence? {
return when (timelineEvent.root.getClearType()) {
EventType.MESSAGE, EventType.MESSAGE,
EventType.STICKER -> { EventType.STICKER -> {
val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent() val messageContent: MessageContent? = timelineEvent.getLastMessageContent()
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
val html = messageContent.formattedBody val html = messageContent.formattedBody
?.takeIf { it.isNotBlank() } ?.takeIf { it.isNotBlank() }
@ -193,41 +185,39 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> { EventType.CALL_ANSWER -> {
timelineEvent()?.let { noticeEventFormatter.format(it) } noticeEventFormatter.format(timelineEvent)
} }
else -> null else -> null
} }
} }
private fun actionsForEvent(optionalEvent: Optional<TimelineEvent>): List<EventSharedAction> { private fun actionsForEvent(timelineEvent: TimelineEvent): List<EventSharedAction> {
val event = optionalEvent.getOrNull() ?: return emptyList() val messageContent: MessageContent? = timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: timelineEvent.root.getClearContent().toModel()
val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel()
?: event.root.getClearContent().toModel()
val type = messageContent?.type val type = messageContent?.type
return arrayListOf<EventSharedAction>().apply { return arrayListOf<EventSharedAction>().apply {
if (event.root.sendState.hasFailed()) { if (timelineEvent.root.sendState.hasFailed()) {
if (canRetry(event)) { if (canRetry(timelineEvent)) {
add(EventSharedAction.Resend(eventId)) add(EventSharedAction.Resend(eventId))
} }
add(EventSharedAction.Remove(eventId)) add(EventSharedAction.Remove(eventId))
} else if (event.root.sendState.isSending()) { } else if (timelineEvent.root.sendState.isSending()) {
// TODO is uploading attachment? // TODO is uploading attachment?
if (canCancel(event)) { if (canCancel(timelineEvent)) {
add(EventSharedAction.Cancel(eventId)) add(EventSharedAction.Cancel(eventId))
} }
} else if (event.root.sendState == SendState.SYNCED) { } else if (timelineEvent.root.sendState == SendState.SYNCED) {
if (!event.root.isRedacted()) { if (!timelineEvent.root.isRedacted()) {
if (canReply(event, messageContent)) { if (canReply(timelineEvent, messageContent)) {
add(EventSharedAction.Reply(eventId)) add(EventSharedAction.Reply(eventId))
} }
if (canEdit(event, session.myUserId)) { if (canEdit(timelineEvent, session.myUserId)) {
add(EventSharedAction.Edit(eventId)) add(EventSharedAction.Edit(eventId))
} }
if (canRedact(event, session.myUserId)) { if (canRedact(timelineEvent, session.myUserId)) {
add(EventSharedAction.Delete(eventId)) add(EventSharedAction.Delete(eventId))
} }
@ -236,19 +226,19 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
add(EventSharedAction.Copy(messageContent!!.body)) add(EventSharedAction.Copy(messageContent!!.body))
} }
if (event.canReact()) { if (timelineEvent.canReact()) {
add(EventSharedAction.AddReaction(eventId)) add(EventSharedAction.AddReaction(eventId))
} }
if (canQuote(event, messageContent)) { if (canQuote(timelineEvent, messageContent)) {
add(EventSharedAction.Quote(eventId)) add(EventSharedAction.Quote(eventId))
} }
if (canViewReactions(event)) { if (canViewReactions(timelineEvent)) {
add(EventSharedAction.ViewReactions(informationData)) add(EventSharedAction.ViewReactions(informationData))
} }
if (event.hasBeenEdited()) { if (timelineEvent.hasBeenEdited()) {
add(EventSharedAction.ViewEditHistory(informationData)) add(EventSharedAction.ViewEditHistory(informationData))
} }
@ -261,7 +251,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
// TODO // TODO
} }
if (event.root.sendState == SendState.SENT) { if (timelineEvent.root.sendState == SendState.SENT) {
// TODO Can be redacted // TODO Can be redacted
// TODO sent by me or sufficient power level // TODO sent by me or sufficient power level
@ -269,24 +259,22 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
} }
if (vectorPreferences.developerMode()) { if (vectorPreferences.developerMode()) {
add(EventSharedAction.ViewSource(event.root.toContentStringWithIndent())) add(EventSharedAction.ViewSource(timelineEvent.root.toContentStringWithIndent()))
if (event.isEncrypted()) { if (timelineEvent.isEncrypted()) {
val decryptedContent = event.root.toClearContentStringWithIndent() val decryptedContent = timelineEvent.root.toClearContentStringWithIndent()
?: stringProvider.getString(R.string.encryption_information_decryption_error) ?: stringProvider.getString(R.string.encryption_information_decryption_error)
add(EventSharedAction.ViewDecryptedSource(decryptedContent)) add(EventSharedAction.ViewDecryptedSource(decryptedContent))
} }
} }
add(EventSharedAction.CopyPermalink(eventId)) add(EventSharedAction.CopyPermalink(eventId))
if (session.myUserId != timelineEvent.root.senderId) {
if (session.myUserId != event.root.senderId) {
// not sent by me // not sent by me
if (event.root.getClearType() == EventType.MESSAGE) { if (timelineEvent.root.getClearType() == EventType.MESSAGE) {
add(EventSharedAction.ReportContent(eventId, event.root.senderId)) add(EventSharedAction.ReportContent(eventId, timelineEvent.root.senderId))
} }
add(EventSharedAction.Separator) add(EventSharedAction.Separator)
add(EventSharedAction.IgnoreUser(event.root.senderId)) add(EventSharedAction.IgnoreUser(timelineEvent.root.senderId))
} }
} }
} }

View file

@ -128,8 +128,8 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
} }
private fun formatRoomMemberEvent(event: Event, senderName: String?): String? { private fun formatRoomMemberEvent(event: Event, senderName: String?): String? {
val eventContent: RoomMember? = event.getClearContent().toModel() val eventContent: RoomMemberContent? = event.getClearContent().toModel()
val prevEventContent: RoomMember? = event.prevContent.toModel() val prevEventContent: RoomMemberContent? = event.prevContent.toModel()
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
return if (isMembershipEvent) { return if (isMembershipEvent) {
buildMembershipNotice(event, senderName, eventContent, prevEventContent) buildMembershipNotice(event, senderName, eventContent, prevEventContent)
@ -166,7 +166,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
?: sp.getString(R.string.notice_room_canonical_alias_unset, senderName) ?: sp.getString(R.string.notice_room_canonical_alias_unset, senderName)
} }
private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String { private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMemberContent?, prevEventContent: RoomMemberContent?): String {
val displayText = StringBuilder() val displayText = StringBuilder()
// Check display name has been changed // Check display name has been changed
if (eventContent?.displayName != prevEventContent?.displayName) { if (eventContent?.displayName != prevEventContent?.displayName) {
@ -198,7 +198,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
return displayText.toString() return displayText.toString()
} }
private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? { private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMemberContent?, prevEventContent: RoomMemberContent?): String? {
val senderDisplayName = senderName ?: event.senderId ?: "" val senderDisplayName = senderName ?: event.senderId ?: ""
val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: "" val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: ""
return when (eventContent?.membership) { return when (eventContent?.membership) {

View file

@ -49,7 +49,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
holder.messageView.setOnClickListener(attributes.itemClickListener) holder.messageView.setOnClickListener(attributes.itemClickListener)
holder.messageView.setOnLongClickListener(attributes.itemLongClickListener) holder.messageView.setOnLongClickListener(attributes.itemLongClickListener)
if (searchForPills) { if (searchForPills) {
message?.findPillsAndProcess { it.bind(holder.messageView) } message?.findPillsAndProcess(coroutineScope) { it.bind(holder.messageView) }
} }
val textFuture = PrecomputedTextCompat.getTextFuture( val textFuture = PrecomputedTextCompat.getTextFuture(
message ?: "", message ?: "",

View file

@ -25,14 +25,11 @@ import im.vector.riotx.core.linkify.VectorLinkify
import im.vector.riotx.core.utils.isValidUrl import im.vector.riotx.core.utils.isValidUrl
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.html.PillImageSpan
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.saket.bettermovementmethod.BetterLinkMovementMethod import me.saket.bettermovementmethod.BetterLinkMovementMethod
fun CharSequence.findPillsAndProcess(processBlock: (PillImageSpan) -> Unit) { fun CharSequence.findPillsAndProcess(scope: CoroutineScope, processBlock: (PillImageSpan) -> Unit) {
GlobalScope.launch(Dispatchers.Main) { scope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
toSpannable().let { spannable -> toSpannable().let { spannable ->
spannable.getSpans(0, spannable.length, PillImageSpan::class.java) spannable.getSpans(0, spannable.length, PillImageSpan::class.java)

View file

@ -59,7 +59,8 @@ data class RoomListParams(
class RoomListFragment @Inject constructor( class RoomListFragment @Inject constructor(
private val roomController: RoomSummaryController, private val roomController: RoomSummaryController,
val roomListViewModelFactory: RoomListViewModel.Factory, val roomListViewModelFactory: RoomListViewModel.Factory,
private val notificationDrawerManager: NotificationDrawerManager private val notificationDrawerManager: NotificationDrawerManager,
private val sharedViewPool: RecyclerView.RecycledViewPool
) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener { ) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {
@ -95,7 +96,6 @@ class RoomListFragment @Inject constructor(
setupCreateRoomButton() setupCreateRoomButton()
setupRecyclerView() setupRecyclerView()
sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
roomListViewModel.subscribe { renderState(it) } roomListViewModel.subscribe { renderState(it) }
roomListViewModel.viewEvents roomListViewModel.viewEvents
.observe() .observe()
@ -193,6 +193,8 @@ class RoomListFragment @Inject constructor(
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
roomListView.layoutManager = layoutManager roomListView.layoutManager = layoutManager
roomListView.itemAnimator = RoomListAnimator() roomListView.itemAnimator = RoomListAnimator()
roomListView.setRecycledViewPool(sharedViewPool)
layoutManager.recycleChildrenOnDetach = true
roomController.listener = this roomController.listener = this
modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) } modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) }
roomController.addModelBuildListener(modelBuildListener) roomController.addModelBuildListener(modelBuildListener)

View file

@ -46,6 +46,7 @@ data class RoomListActionsArgs(
class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomListQuickActionsEpoxyController.Listener { class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), RoomListQuickActionsEpoxyController.Listener {
private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel
@Inject lateinit var sharedViewPool: RecyclerView.RecycledViewPool
@Inject lateinit var roomListActionsViewModelFactory: RoomListQuickActionsViewModel.Factory @Inject lateinit var roomListActionsViewModelFactory: RoomListQuickActionsViewModel.Factory
@Inject lateinit var roomListActionsEpoxyController: RoomListQuickActionsEpoxyController @Inject lateinit var roomListActionsEpoxyController: RoomListQuickActionsEpoxyController
@Inject lateinit var navigator: Navigator @Inject lateinit var navigator: Navigator
@ -70,7 +71,7 @@ class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), R
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
recyclerView.configureWith(roomListActionsEpoxyController, hasFixedSize = false) recyclerView.configureWith(roomListActionsEpoxyController, viewPool = sharedViewPool, hasFixedSize = false)
// Disable item animation // Disable item animation
recyclerView.itemAnimator = null recyclerView.itemAnimator = null
roomListActionsEpoxyController.listener = this roomListActionsEpoxyController.listener = this

View file

@ -88,25 +88,27 @@ class PillImageSpan(private val glideRequests: GlideRequests,
} }
internal fun updateAvatarDrawable(drawable: Drawable?) { internal fun updateAvatarDrawable(drawable: Drawable?) {
pillDrawable.apply { pillDrawable.chipIcon = drawable
chipIcon = drawable tv?.get()?.invalidate()
}
tv?.get()?.apply {
invalidate()
}
} }
// Private methods ***************************************************************************** // Private methods *****************************************************************************
private fun createChipDrawable(): ChipDrawable { private fun createChipDrawable(): ChipDrawable {
val textPadding = context.resources.getDimension(R.dimen.pill_text_padding) val textPadding = context.resources.getDimension(R.dimen.pill_text_padding)
val icon = try {
avatarRenderer.getCachedDrawable(glideRequests, matrixItem)
} catch (exception: Exception) {
avatarRenderer.getPlaceholderDrawable(context, matrixItem)
}
return ChipDrawable.createFromResource(context, R.xml.pill_view).apply { return ChipDrawable.createFromResource(context, R.xml.pill_view).apply {
text = matrixItem.getBestName() text = matrixItem.getBestName()
textEndPadding = textPadding textEndPadding = textPadding
textStartPadding = textPadding textStartPadding = textPadding
setChipMinHeightResource(R.dimen.pill_min_height) setChipMinHeightResource(R.dimen.pill_min_height)
setChipIconSizeResource(R.dimen.pill_avatar_size) setChipIconSizeResource(R.dimen.pill_avatar_size)
chipIcon = avatarRenderer.getPlaceholderDrawable(context, matrixItem) chipIcon = icon
setBounds(0, 0, intrinsicWidth, intrinsicHeight) setBounds(0, 0, intrinsicWidth, intrinsicHeight)
} }
} }

View file

@ -23,7 +23,7 @@ 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.toModel import im.vector.matrix.android.api.session.events.model.toModel
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.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomMemberContent
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.getEditedEventId import im.vector.matrix.android.api.session.room.timeline.getEditedEventId
import im.vector.matrix.android.api.session.room.timeline.getLastMessageBody import im.vector.matrix.android.api.session.room.timeline.getLastMessageBody
@ -163,7 +163,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
} }
private fun resolveStateRoomEvent(event: Event, session: Session): NotifiableEvent? { private fun resolveStateRoomEvent(event: Event, session: Session): NotifiableEvent? {
val content = event.content?.toModel<RoomMember>() ?: return null val content = event.content?.toModel<RoomMemberContent>() ?: return null
val roomId = event.roomId ?: return null val roomId = event.roomId ?: return null
val dName = event.senderId?.let { session.getUser(it)?.displayName } val dName = event.senderId?.let { session.getUser(it)?.displayName }
if (Membership.INVITE == content.membership) { if (Membership.INVITE == content.membership) {

View file

@ -18,10 +18,10 @@ package im.vector.riotx.features.reactions.data
import android.content.res.Resources import android.content.res.Resources
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenScope
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton
@ScreenScope @Singleton
class EmojiDataSource @Inject constructor( class EmojiDataSource @Inject constructor(
resources: Resources resources: Resources
) { ) {
@ -41,6 +41,7 @@ class EmojiDataSource @Inject constructor(
// First add emojis with name matching query, sorted by name // First add emojis with name matching query, sorted by name
return (rawData.emojis.values return (rawData.emojis.values
.asSequence()
.filter { emojiItem -> .filter { emojiItem ->
emojiItem.name.contains(query, true) emojiItem.name.contains(query, true)
} }
@ -55,6 +56,7 @@ class EmojiDataSource @Inject constructor(
.sortedBy { it.name }) .sortedBy { it.name })
// and ensure they will not be present twice // and ensure they will not be present twice
.distinct() .distinct()
.toList()
} }
fun getQuickReactions(): List<EmojiItem> { fun getQuickReactions(): List<EmojiItem> {

View file

@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRooms
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse
import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.extensions.postLiveEvent
@ -79,13 +80,14 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
} }
private fun observeJoinedRooms() { private fun observeJoinedRooms() {
val queryParams = roomSummaryQueryParams {
memberships = listOf(Membership.JOIN)
}
session session
.rx() .rx()
.liveRoomSummaries() .liveRoomSummaries(queryParams)
.subscribe { list -> .subscribe { list ->
val joinedRoomIds = list val joinedRoomIds = list
// Keep only joined room
?.filter { it.membership == Membership.JOIN }
?.map { it.roomId } ?.map { it.roomId }
?.toSet() ?.toSet()
?: emptySet() ?: emptySet()
@ -106,9 +108,9 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState:
override fun handle(action: RoomDirectoryAction) { override fun handle(action: RoomDirectoryAction) {
when (action) { when (action) {
is RoomDirectoryAction.SetRoomDirectoryData -> setRoomDirectoryData(action) is RoomDirectoryAction.SetRoomDirectoryData -> setRoomDirectoryData(action)
is RoomDirectoryAction.FilterWith -> filterWith(action) is RoomDirectoryAction.FilterWith -> filterWith(action)
RoomDirectoryAction.LoadMore -> loadMore() RoomDirectoryAction.LoadMore -> loadMore()
is RoomDirectoryAction.JoinRoom -> joinRoom(action) is RoomDirectoryAction.JoinRoom -> joinRoom(action)
} }
} }

Some files were not shown because too many files have changed in this diff Show more