Merge pull request #3092 from vector-im/feature/bca/paged_room_list

Room List performance PR (use Live PagedList via Monarchy)
This commit is contained in:
Benoit Marty 2021-04-06 14:13:47 +02:00 committed by GitHub
commit 3109d111a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1233 additions and 608 deletions

View file

@ -14,6 +14,7 @@ Improvements 🙌:
- Update reactions to Unicode 13.1 (#2998) - Update reactions to Unicode 13.1 (#2998)
- Be more robust when parsing some enums - Be more robust when parsing some enums
- Improve timeline filtering (dissociate membership and profile events, display hidden events when highlighted, fix hidden item/read receipts behavior) - Improve timeline filtering (dissociate membership and profile events, display hidden events when highlighted, fix hidden item/read receipts behavior)
- Room list improvements (paging)
Bugfix 🐛: Bugfix 🐛:
- Fix bad theme change for the MainActivity - Fix bad theme change for the MainActivity

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.query
enum class RoomCategoryFilter {
ONLY_DM,
ONLY_ROOMS,
ONLY_WITH_NOTIFICATIONS,
ALL
}

View file

@ -0,0 +1,23 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.query
data class RoomTagQueryFilter(
val isFavorite: Boolean?,
val isLowPriority: Boolean?,
val isServerNotice: Boolean?
)

View file

@ -17,6 +17,7 @@
package org.matrix.android.sdk.api.session.room package org.matrix.android.sdk.api.session.room
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
@ -24,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.peeking.PeekResult import org.matrix.android.sdk.api.session.room.peeking.PeekResult
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
@ -178,4 +180,29 @@ interface RoomService {
* This call will try to gather some information on this room, but it could fail and get nothing more * This call will try to gather some information on this room, but it could fail and get nothing more
*/ */
fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback<PeekResult>) fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback<PeekResult>)
/**
* TODO Doc
*/
fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams,
pagedListConfig: PagedList.Config = defaultPagedListConfig): LiveData<PagedList<RoomSummary>>
/**
* TODO Doc
*/
fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams,
pagedListConfig: PagedList.Config = defaultPagedListConfig): UpdatableFilterLivePageResult
/**
* TODO Doc
*/
fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount
private val defaultPagedListConfig
get() = PagedList.Config.Builder()
.setPageSize(10)
.setInitialLoadSizeHint(20)
.setEnablePlaceholders(false)
.setPrefetchDistance(10)
.build()
} }

View file

@ -17,6 +17,8 @@
package org.matrix.android.sdk.api.session.room package org.matrix.android.sdk.api.session.room
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.query.RoomCategoryFilter
import org.matrix.android.sdk.api.query.RoomTagQueryFilter
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams { fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams {
@ -31,7 +33,9 @@ data class RoomSummaryQueryParams(
val roomId: QueryStringValue, val roomId: QueryStringValue,
val displayName: QueryStringValue, val displayName: QueryStringValue,
val canonicalAlias: QueryStringValue, val canonicalAlias: QueryStringValue,
val memberships: List<Membership> val memberships: List<Membership>,
val roomCategoryFilter: RoomCategoryFilter?,
val roomTagQueryFilter: RoomTagQueryFilter?
) { ) {
class Builder { class Builder {
@ -40,12 +44,16 @@ data class RoomSummaryQueryParams(
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
var memberships: List<Membership> = Membership.all() var memberships: List<Membership> = Membership.all()
var roomCategoryFilter: RoomCategoryFilter? = RoomCategoryFilter.ALL
var roomTagQueryFilter: RoomTagQueryFilter? = null
fun build() = RoomSummaryQueryParams( fun build() = RoomSummaryQueryParams(
roomId = roomId, roomId = roomId,
displayName = displayName, displayName = displayName,
canonicalAlias = canonicalAlias, canonicalAlias = canonicalAlias,
memberships = memberships memberships = memberships,
roomCategoryFilter = roomCategoryFilter,
roomTagQueryFilter = roomTagQueryFilter
) )
} }
} }

View file

@ -1,11 +1,11 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright (c) 2021 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.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -14,12 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.home package org.matrix.android.sdk.api.session.room
import im.vector.app.core.utils.BehaviorDataSource import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import javax.inject.Inject
import javax.inject.Singleton
@Singleton interface UpdatableFilterLivePageResult {
class HomeRoomListDataSource @Inject constructor() : BehaviorDataSource<List<RoomSummary>>() val livePagedList: LiveData<PagedList<RoomSummary>>
fun updateQuery(queryParams: RoomSummaryQueryParams)
}

View file

@ -0,0 +1,25 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.summary
data class RoomAggregateNotificationCount(
val notificationCount: Int,
val highlightCount: Int
) {
val totalCount = notificationCount + highlightCount
val isHighlight = highlightCount > 0
}

View file

@ -17,22 +17,27 @@
package org.matrix.android.sdk.internal.database package org.matrix.android.sdk.internal.database
import io.realm.DynamicRealm import io.realm.DynamicRealm
import io.realm.FieldAttribute
import io.realm.RealmMigration import io.realm.RealmMigration
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.EditionOfEventFields import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
import org.matrix.android.sdk.internal.database.model.RoomEntityFields import org.matrix.android.sdk.internal.database.model.RoomEntityFields
import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomTagEntityFields
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class RealmSessionStoreMigration @Inject constructor() : RealmMigration { class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
companion object { companion object {
const val SESSION_STORE_SCHEMA_VERSION = 8L const val SESSION_STORE_SCHEMA_VERSION = 9L
} }
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
@ -46,6 +51,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
if (oldVersion <= 5) migrateTo6(realm) if (oldVersion <= 5) migrateTo6(realm)
if (oldVersion <= 6) migrateTo7(realm) if (oldVersion <= 6) migrateTo7(realm)
if (oldVersion <= 7) migrateTo8(realm) if (oldVersion <= 7) migrateTo8(realm)
if (oldVersion <= 8) migrateTo9(realm)
} }
private fun migrateTo1(realm: DynamicRealm) { private fun migrateTo1(realm: DynamicRealm) {
@ -149,4 +155,43 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
?.removeField("sourceLocalEchoEvents") ?.removeField("sourceLocalEchoEvents")
?.addRealmListField(EditAggregatedSummaryEntityFields.EDITIONS.`$`, editionOfEventSchema) ?.addRealmListField(EditAggregatedSummaryEntityFields.EDITIONS.`$`, editionOfEventSchema)
} }
fun migrateTo9(realm: DynamicRealm) {
Timber.d("Step 8 -> 9")
realm.schema.get("RoomSummaryEntity")
?.addField(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Long::class.java, FieldAttribute.INDEXED)
?.setNullable(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, true)
?.addIndex(RoomSummaryEntityFields.MEMBERSHIP_STR)
?.addIndex(RoomSummaryEntityFields.IS_DIRECT)
?.addIndex(RoomSummaryEntityFields.VERSIONING_STATE_STR)
?.addField(RoomSummaryEntityFields.IS_FAVOURITE, Boolean::class.java)
?.addIndex(RoomSummaryEntityFields.IS_FAVOURITE)
?.addField(RoomSummaryEntityFields.IS_LOW_PRIORITY, Boolean::class.java)
?.addIndex(RoomSummaryEntityFields.IS_LOW_PRIORITY)
?.addField(RoomSummaryEntityFields.IS_SERVER_NOTICE, Boolean::class.java)
?.addIndex(RoomSummaryEntityFields.IS_SERVER_NOTICE)
?.transform { obj ->
val isFavorite = obj.getList(RoomSummaryEntityFields.TAGS.`$`).any {
it.getString(RoomTagEntityFields.TAG_NAME) == RoomTag.ROOM_TAG_FAVOURITE
}
obj.setBoolean(RoomSummaryEntityFields.IS_FAVOURITE, isFavorite)
val isLowPriority = obj.getList(RoomSummaryEntityFields.TAGS.`$`).any {
it.getString(RoomTagEntityFields.TAG_NAME) == RoomTag.ROOM_TAG_LOW_PRIORITY
}
obj.setBoolean(RoomSummaryEntityFields.IS_LOW_PRIORITY, isLowPriority)
// XXX migrate last message origin server ts
obj.getObject(RoomSummaryEntityFields.LATEST_PREVIEWABLE_EVENT.`$`)
?.getObject(TimelineEventEntityFields.ROOT.`$`)
?.getLong(EventEntityFields.ORIGIN_SERVER_TS)?.let {
obj.setLong(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, it)
}
}
}
} }

View file

@ -26,7 +26,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
private val typingUsersTracker: DefaultTypingUsersTracker) { private val typingUsersTracker: DefaultTypingUsersTracker) {
fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary {
val tags = roomSummaryEntity.tags.map { val tags = roomSummaryEntity.tags().map {
RoomTag(it.tagName, it.tagOrder) RoomTag(it.tagName, it.tagOrder)
} }

View file

@ -16,61 +16,217 @@
package org.matrix.android.sdk.internal.database.model package org.matrix.android.sdk.internal.database.model
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.Index
import io.realm.annotations.PrimaryKey
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.room.model.VersioningState
import io.realm.RealmList import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class RoomSummaryEntity( internal open class RoomSummaryEntity(
@PrimaryKey var roomId: String = "", @PrimaryKey var roomId: String = ""
var displayName: String? = "",
var avatarUrl: String? = "",
var name: String? = "",
var topic: String? = "",
var latestPreviewableEvent: TimelineEventEntity? = null,
var heroes: RealmList<String> = RealmList(),
var joinedMembersCount: Int? = 0,
var invitedMembersCount: Int? = 0,
var isDirect: Boolean = false,
var directUserId: String? = null,
var otherMemberIds: RealmList<String> = RealmList(),
var notificationCount: Int = 0,
var highlightCount: Int = 0,
var readMarkerId: String? = null,
var hasUnreadMessages: Boolean = false,
var tags: RealmList<RoomTagEntity> = RealmList(),
var userDrafts: UserDraftsEntity? = null,
var breadcrumbsIndex: Int = RoomSummary.NOT_IN_BREADCRUMBS,
var canonicalAlias: String? = null,
var aliases: RealmList<String> = RealmList(),
// this is required for querying
var flatAliases: String = "",
var isEncrypted: Boolean = false,
var encryptionEventTs: Long? = 0,
var roomEncryptionTrustLevelStr: String? = null,
var inviterId: String? = null,
var hasFailedSending: Boolean = false
) : RealmObject() { ) : RealmObject() {
var displayName: String? = ""
set(value) {
if (value != field) field = value
}
var avatarUrl: String? = ""
set(value) {
if (value != field) field = value
}
var name: String? = ""
set(value) {
if (value != field) field = value
}
var topic: String? = ""
set(value) {
if (value != field) field = value
}
var latestPreviewableEvent: TimelineEventEntity? = null
set(value) {
if (value != field) field = value
}
@Index
var lastActivityTime: Long? = null
set(value) {
if (value != field) field = value
}
var heroes: RealmList<String> = RealmList()
var joinedMembersCount: Int? = 0
set(value) {
if (value != field) field = value
}
var invitedMembersCount: Int? = 0
set(value) {
if (value != field) field = value
}
@Index
var isDirect: Boolean = false
set(value) {
if (value != field) field = value
}
var directUserId: String? = null
set(value) {
if (value != field) field = value
}
var otherMemberIds: RealmList<String> = RealmList()
var notificationCount: Int = 0
set(value) {
if (value != field) field = value
}
var highlightCount: Int = 0
set(value) {
if (value != field) field = value
}
var readMarkerId: String? = null
set(value) {
if (value != field) field = value
}
var hasUnreadMessages: Boolean = false
set(value) {
if (value != field) field = value
}
private var tags: RealmList<RoomTagEntity> = RealmList()
fun tags(): List<RoomTagEntity> = tags
fun updateTags(newTags: List<Pair<String, Double?>>) {
val toDelete = mutableListOf<RoomTagEntity>()
tags.forEach { existingTag ->
val updatedTag = newTags.firstOrNull { it.first == existingTag.tagName }
if (updatedTag == null) {
toDelete.add(existingTag)
} else {
existingTag.tagOrder = updatedTag.second
}
}
toDelete.forEach { it.deleteFromRealm() }
newTags.forEach { newTag ->
if (tags.all { it.tagName != newTag.first }) {
// we must add it
tags.add(
RoomTagEntity(newTag.first, newTag.second)
)
}
}
isFavourite = newTags.any { it.first == RoomTag.ROOM_TAG_FAVOURITE }
isLowPriority = newTags.any { it.first == RoomTag.ROOM_TAG_LOW_PRIORITY }
isServerNotice = newTags.any { it.first == RoomTag.ROOM_TAG_SERVER_NOTICE }
}
@Index
var isFavourite: Boolean = false
set(value) {
if (value != field) field = value
}
@Index
var isLowPriority: Boolean = false
set(value) {
if (value != field) field = value
}
@Index
var isServerNotice: Boolean = false
set(value) {
if (value != field) field = value
}
var userDrafts: UserDraftsEntity? = null
set(value) {
if (value != field) field = value
}
var breadcrumbsIndex: Int = RoomSummary.NOT_IN_BREADCRUMBS
set(value) {
if (value != field) field = value
}
var canonicalAlias: String? = null
set(value) {
if (value != field) field = value
}
var aliases: RealmList<String> = RealmList()
fun updateAliases(newAliases: List<String>) {
// only update underlying field if there is a diff
if (newAliases.distinct().sorted() != aliases.distinct().sorted()) {
aliases.clear()
aliases.addAll(newAliases)
flatAliases = newAliases.joinToString(separator = "|", prefix = "|")
}
}
// this is required for querying
var flatAliases: String = ""
var isEncrypted: Boolean = false
set(value) {
if (value != field) field = value
}
var encryptionEventTs: Long? = 0
set(value) {
if (value != field) field = value
}
var roomEncryptionTrustLevelStr: String? = null
set(value) {
if (value != field) field = value
}
var inviterId: String? = null
set(value) {
if (value != field) field = value
}
var hasFailedSending: Boolean = false
set(value) {
if (value != field) field = value
}
@Index
private var membershipStr: String = Membership.NONE.name private var membershipStr: String = Membership.NONE.name
var membership: Membership var membership: Membership
get() { get() {
return Membership.valueOf(membershipStr) return Membership.valueOf(membershipStr)
} }
set(value) { set(value) {
membershipStr = value.name if (value.name != membershipStr) {
membershipStr = value.name
}
} }
@Index
private var versioningStateStr: String = VersioningState.NONE.name private var versioningStateStr: String = VersioningState.NONE.name
var versioningState: VersioningState var versioningState: VersioningState
get() { get() {
return VersioningState.valueOf(versioningStateStr) return VersioningState.valueOf(versioningStateStr)
} }
set(value) { set(value) {
versioningStateStr = value.name if (value.name != versioningStateStr) {
versioningStateStr = value.name
}
} }
var roomEncryptionTrustLevel: RoomEncryptionTrustLevel? var roomEncryptionTrustLevel: RoomEncryptionTrustLevel?
@ -84,7 +240,9 @@ internal open class RoomSummaryEntity(
} }
} }
set(value) { set(value) {
roomEncryptionTrustLevelStr = value?.name if (value?.name != roomEncryptionTrustLevelStr) {
roomEncryptionTrustLevelStr = value?.name
}
} }
companion object companion object

View file

@ -18,17 +18,20 @@ package org.matrix.android.sdk.internal.session.room
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import androidx.paging.PagedList
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.RoomService import org.matrix.android.sdk.api.session.room.RoomService
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.UpdatableFilterLivePageResult
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.peeking.PeekResult import org.matrix.android.sdk.api.session.room.peeking.PeekResult
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
@ -96,6 +99,20 @@ internal class DefaultRoomService @Inject constructor(
return roomSummaryDataSource.getRoomSummariesLive(queryParams) return roomSummaryDataSource.getRoomSummariesLive(queryParams)
} }
override fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config)
: LiveData<PagedList<RoomSummary>> {
return roomSummaryDataSource.getSortedPagedRoomSummariesLive(queryParams, pagedListConfig)
}
override fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config)
: UpdatableFilterLivePageResult {
return roomSummaryDataSource.getFilteredPagedRoomSummariesLive(queryParams, pagedListConfig)
}
override fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
return roomSummaryDataSource.getNotificationCountForRooms(queryParams)
}
override fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List<RoomSummary> { override fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List<RoomSummary> {
return roomSummaryDataSource.getBreadcrumbs(queryParams) return roomSummaryDataSource.getBreadcrumbs(queryParams)
} }

View file

@ -17,18 +17,19 @@
package org.matrix.android.sdk.internal.session.room.create package org.matrix.android.sdk.internal.session.room.create
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.session.room.alias.RoomAliasError import org.matrix.android.sdk.api.session.room.alias.RoomAliasError
import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.RoomEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
@ -96,12 +97,18 @@ internal class DefaultCreateRoomTask @Inject constructor(
// Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before) // Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before)
try { try {
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
realm.where(RoomEntity::class.java) realm.where(RoomSummaryEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, roomId) .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId)
.equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
} }
} catch (exception: TimeoutCancellationException) { } catch (exception: TimeoutCancellationException) {
throw CreateRoomFailure.CreatedWithTimeout throw CreateRoomFailure.CreatedWithTimeout
} }
Realm.getInstance(realmConfiguration).executeTransactionAsync {
RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = System.currentTimeMillis()
}
if (otherUserId != null) { if (otherUserId != null) {
handleDirectChatCreation(roomId, otherUserId) handleDirectChatCreation(roomId, otherUserId)
} }

View file

@ -16,20 +16,23 @@
package org.matrix.android.sdk.internal.session.room.membership.joining package org.matrix.android.sdk.internal.session.room.membership.joining
import io.realm.Realm
import io.realm.RealmConfiguration
import kotlinx.coroutines.TimeoutCancellationException
import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
import org.matrix.android.sdk.internal.session.room.read.SetReadMarkersTask import org.matrix.android.sdk.internal.session.room.read.SetReadMarkersTask
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import io.realm.RealmConfiguration
import kotlinx.coroutines.TimeoutCancellationException
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@ -68,12 +71,18 @@ internal class DefaultJoinRoomTask @Inject constructor(
val roomId = joinRoomResponse.roomId val roomId = joinRoomResponse.roomId
try { try {
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
realm.where(RoomEntity::class.java) realm.where(RoomSummaryEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, roomId) .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId)
.equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
} }
} catch (exception: TimeoutCancellationException) { } catch (exception: TimeoutCancellationException) {
throw JoinRoomFailure.JoinedWithTimeout throw JoinRoomFailure.JoinedWithTimeout
} }
Realm.getInstance(realmConfiguration).executeTransactionAsync {
RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = System.currentTimeMillis()
}
setReadMarkers(roomId) setReadMarkers(roomId)
} }

View file

@ -18,10 +18,18 @@ package org.matrix.android.sdk.internal.session.room.summary
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.Sort
import org.matrix.android.sdk.api.query.RoomCategoryFilter
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.UpdatableFilterLivePageResult
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.room.model.VersioningState
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper
@ -32,8 +40,6 @@ import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.query.process import org.matrix.android.sdk.internal.query.process
import org.matrix.android.sdk.internal.util.fetchCopyMap import org.matrix.android.sdk.internal.util.fetchCopyMap
import io.realm.Realm
import io.realm.RealmQuery
import javax.inject.Inject import javax.inject.Inject
internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy, internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy,
@ -98,6 +104,62 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
.sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX) .sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX)
} }
fun getSortedPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams,
pagedListConfig: PagedList.Config): LiveData<PagedList<RoomSummary>> {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
roomSummariesQuery(realm, queryParams)
.sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING)
}
val dataSourceFactory = realmDataSourceFactory.map {
roomSummaryMapper.map(it)
}
return monarchy.findAllPagedWithChanges(
realmDataSourceFactory,
LivePagedListBuilder(dataSourceFactory, pagedListConfig)
)
}
fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams,
pagedListConfig: PagedList.Config): UpdatableFilterLivePageResult {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
roomSummariesQuery(realm, queryParams)
.sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING)
}
val dataSourceFactory = realmDataSourceFactory.map {
roomSummaryMapper.map(it)
}
val mapped = monarchy.findAllPagedWithChanges(
realmDataSourceFactory,
LivePagedListBuilder(dataSourceFactory, pagedListConfig)
)
return object : UpdatableFilterLivePageResult {
override val livePagedList: LiveData<PagedList<RoomSummary>> = mapped
override fun updateQuery(queryParams: RoomSummaryQueryParams) {
realmDataSourceFactory.updateQuery {
roomSummariesQuery(it, queryParams)
.sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING)
}
}
}
}
fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
var notificationCount: RoomAggregateNotificationCount? = null
monarchy.doWithRealm { realm ->
val roomSummariesQuery = roomSummariesQuery(realm, queryParams)
val notifCount = roomSummariesQuery.sum(RoomSummaryEntityFields.NOTIFICATION_COUNT).toInt()
val highlightCount = roomSummariesQuery.sum(RoomSummaryEntityFields.HIGHLIGHT_COUNT).toInt()
notificationCount = RoomAggregateNotificationCount(
notifCount,
highlightCount
)
}
return notificationCount!!
}
private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery<RoomSummaryEntity> { private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery<RoomSummaryEntity> {
val query = RoomSummaryEntity.where(realm) val query = RoomSummaryEntity.where(realm)
query.process(RoomSummaryEntityFields.ROOM_ID, queryParams.roomId) query.process(RoomSummaryEntityFields.ROOM_ID, queryParams.roomId)
@ -105,6 +167,28 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
query.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias) query.process(RoomSummaryEntityFields.CANONICAL_ALIAS, queryParams.canonicalAlias)
query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) query.process(RoomSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)
query.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) query.notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name)
queryParams.roomCategoryFilter?.let {
when (it) {
RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0)
RoomCategoryFilter.ALL -> {
// nop
}
}
}
queryParams.roomTagQueryFilter?.let {
it.isFavorite?.let { fav ->
query.equalTo(RoomSummaryEntityFields.IS_FAVOURITE, fav)
}
it.isLowPriority?.let { lp ->
query.equalTo(RoomSummaryEntityFields.IS_LOW_PRIORITY, lp)
}
it.isServerNotice?.let { sn ->
query.equalTo(RoomSummaryEntityFields.IS_SERVER_NOTICE, sn)
}
}
return query return query
} }
} }

View file

@ -98,6 +98,11 @@ internal class RoomSummaryUpdater @Inject constructor(
val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
val lastActivityFromEvent = latestPreviewableEvent?.root?.originServerTs
if (lastActivityFromEvent != null) {
roomSummaryEntity.lastActivityTime = lastActivityFromEvent
}
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(realm.configuration, userId, roomId, latestPreviewableEvent?.eventId) || !isEventRead(realm.configuration, userId, roomId, latestPreviewableEvent?.eventId)
@ -112,9 +117,7 @@ internal class RoomSummaryUpdater @Inject constructor(
val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases
.orEmpty() .orEmpty()
roomSummaryEntity.aliases.clear() roomSummaryEntity.updateAliases(roomAliases)
roomSummaryEntity.aliases.addAll(roomAliases)
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
roomSummaryEntity.isEncrypted = encryptionEvent != null roomSummaryEntity.isEncrypted = encryptionEvent != null
roomSummaryEntity.encryptionEventTs = encryptionEvent?.originServerTs roomSummaryEntity.encryptionEventTs = encryptionEvent?.originServerTs

View file

@ -19,8 +19,8 @@ package org.matrix.android.sdk.internal.session.sync
import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomTagEntity import org.matrix.android.sdk.internal.database.model.RoomTagEntity
import org.matrix.android.sdk.internal.database.query.where
import io.realm.Realm import io.realm.Realm
import org.matrix.android.sdk.internal.database.query.getOrCreate
import javax.inject.Inject import javax.inject.Inject
internal class RoomTagHandler @Inject constructor() { internal class RoomTagHandler @Inject constructor() {
@ -31,12 +31,8 @@ internal class RoomTagHandler @Inject constructor() {
} }
val tags = content.tags.entries.map { (tagName, params) -> val tags = content.tags.entries.map { (tagName, params) ->
RoomTagEntity(tagName, params["order"] as? Double) RoomTagEntity(tagName, params["order"] as? Double)
Pair(tagName, params["order"] as? Double)
} }
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() RoomSummaryEntity.getOrCreate(realm, roomId).updateTags(tags)
?: RoomSummaryEntity(roomId)
roomSummaryEntity.tags.clear()
roomSummaryEntity.tags.addAll(tags)
realm.insertOrUpdate(roomSummaryEntity)
} }
} }

View file

@ -161,7 +161,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils # android\.text\.TextUtils
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt
enum class===93 enum class===94
### Do not import temporary legacy classes ### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3 import org.matrix.android.sdk.internal.legacy.riot===3

View file

@ -19,78 +19,26 @@ package im.vector.app
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.OnLifecycleEvent
import arrow.core.Option
import im.vector.app.features.grouplist.ALL_COMMUNITIES_GROUP_ID
import im.vector.app.features.grouplist.SelectedGroupDataSource
import im.vector.app.features.home.HomeRoomListDataSource
import im.vector.app.features.home.room.list.ChronologicalRoomComparator
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.functions.BiFunction
import io.reactivex.rxkotlin.addTo
import org.matrix.android.sdk.api.session.group.model.GroupSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.rx.rx
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
/** /**
* This class handles the global app state. At the moment, it only manages room list. * This class handles the global app state.
* It requires to be added to ProcessLifecycleOwner.get().lifecycle * It requires to be added to ProcessLifecycleOwner.get().lifecycle
*/ */
// TODO Keep this class for now, will maybe be used fro Space
@Singleton @Singleton
class AppStateHandler @Inject constructor( class AppStateHandler @Inject constructor() : LifecycleObserver {
private val sessionDataSource: ActiveSessionDataSource,
private val homeRoomListDataSource: HomeRoomListDataSource,
private val selectedGroupDataSource: SelectedGroupDataSource,
private val chronologicalRoomComparator: ChronologicalRoomComparator) : LifecycleObserver {
private val compositeDisposable = CompositeDisposable() private val compositeDisposable = CompositeDisposable()
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun entersForeground() { fun entersForeground() {
observeRoomsAndGroup()
} }
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun entersBackground() { fun entersBackground() {
compositeDisposable.clear() compositeDisposable.clear()
} }
private fun observeRoomsAndGroup() {
Observable
.combineLatest<List<RoomSummary>, Option<GroupSummary>, List<RoomSummary>>(
sessionDataSource.observe()
.observeOn(AndroidSchedulers.mainThread())
.switchMap {
val query = roomSummaryQueryParams {}
it.orNull()?.rx()?.liveRoomSummaries(query)
?: Observable.just(emptyList())
}
.throttleLast(300, TimeUnit.MILLISECONDS),
selectedGroupDataSource.observe(),
BiFunction { rooms, selectedGroupOption ->
val selectedGroup = selectedGroupOption.orNull()
val filteredRooms = rooms.filter {
if (selectedGroup == null || selectedGroup.groupId == ALL_COMMUNITIES_GROUP_ID) {
true
} else if (it.isDirect) {
it.otherMemberIds
.intersect(selectedGroup.userIds)
.isNotEmpty()
} else {
selectedGroup.roomIds.contains(it.roomId)
}
}
filteredRooms.sortedWith(chronologicalRoomComparator)
}
)
.subscribe {
homeRoomListDataSource.post(it)
}
.addTo(compositeDisposable)
}
} }

View file

@ -35,7 +35,6 @@ import im.vector.app.features.crypto.keysrequest.KeyRequestHandler
import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler import im.vector.app.features.crypto.verification.IncomingVerificationRequestHandler
import im.vector.app.features.grouplist.SelectedGroupDataSource import im.vector.app.features.grouplist.SelectedGroupDataSource
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.HomeRoomListDataSource
import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
@ -113,8 +112,6 @@ interface VectorComponent {
fun errorFormatter(): ErrorFormatter fun errorFormatter(): ErrorFormatter
fun homeRoomListObservableStore(): HomeRoomListDataSource
fun selectedGroupStore(): SelectedGroupDataSource fun selectedGroupStore(): SelectedGroupDataSource
fun roomDetailPendingActionStore(): RoomDetailPendingActionStore fun roomDetailPendingActionStore(): RoomDetailPendingActionStore

View file

@ -127,6 +127,12 @@ abstract class VectorBaseFragment<VB: ViewBinding> : BaseMvRxFragment(), HasScre
Timber.i("onResume Fragment ${javaClass.simpleName}") Timber.i("onResume Fragment ${javaClass.simpleName}")
} }
@CallSuper
override fun onPause() {
super.onPause()
Timber.i("onPause Fragment ${javaClass.simpleName}")
}
@CallSuper @CallSuper
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -149,7 +155,9 @@ abstract class VectorBaseFragment<VB: ViewBinding> : BaseMvRxFragment(), HasScre
super.onDestroyView() super.onDestroyView()
} }
@CallSuper
override fun onDestroy() { override fun onDestroy() {
Timber.i("onDestroy Fragment ${javaClass.simpleName}")
uiDisposables.dispose() uiDisposables.dispose()
super.onDestroy() super.onDestroy()
} }

View file

@ -20,4 +20,5 @@ import im.vector.app.core.platform.VectorViewModelAction
sealed class HomeDetailAction : VectorViewModelAction { sealed class HomeDetailAction : VectorViewModelAction {
data class SwitchDisplayMode(val displayMode: RoomListDisplayMode) : HomeDetailAction() data class SwitchDisplayMode(val displayMode: RoomListDisplayMode) : HomeDetailAction()
object MarkAllRoomsRead : HomeDetailAction()
} }

View file

@ -18,6 +18,8 @@ package im.vector.app.features.home
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -33,8 +35,8 @@ import im.vector.app.core.platform.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.ui.views.CurrentCallsView import im.vector.app.core.ui.views.CurrentCallsView
import im.vector.app.core.ui.views.KnownCallsViewHolder
import im.vector.app.core.ui.views.KeysBackupBanner import im.vector.app.core.ui.views.KeysBackupBanner
import im.vector.app.core.ui.views.KnownCallsViewHolder
import im.vector.app.databinding.FragmentHomeDetailBinding import im.vector.app.databinding.FragmentHomeDetailBinding
import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.SharedKnownCallsViewModel
import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.VectorCallActivity
@ -49,7 +51,6 @@ import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.workers.signout.BannerState import im.vector.app.features.workers.signout.BannerState
import im.vector.app.features.workers.signout.ServerBackupStatusViewModel import im.vector.app.features.workers.signout.ServerBackupStatusViewModel
import im.vector.app.features.workers.signout.ServerBackupStatusViewState import im.vector.app.features.workers.signout.ServerBackupStatusViewState
import org.matrix.android.sdk.api.session.group.model.GroupSummary import org.matrix.android.sdk.api.session.group.model.GroupSummary
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
@ -79,6 +80,32 @@ class HomeDetailFragment @Inject constructor(
private lateinit var sharedActionViewModel: HomeSharedActionViewModel private lateinit var sharedActionViewModel: HomeSharedActionViewModel
private lateinit var sharedCallActionViewModel: SharedKnownCallsViewModel private lateinit var sharedCallActionViewModel: SharedKnownCallsViewModel
private var hasUnreadRooms = false
set(value) {
if (value != field) {
field = value
invalidateOptionsMenu()
}
}
override fun getMenuRes() = R.menu.room_list
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_home_mark_all_as_read -> {
viewModel.handle(HomeDetailAction.MarkAllRoomsRead)
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onPrepareOptionsMenu(menu: Menu) {
menu.findItem(R.id.menu_home_mark_all_as_read).isVisible = hasUnreadRooms
super.onPrepareOptionsMenu(menu)
}
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeDetailBinding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeDetailBinding {
return FragmentHomeDetailBinding.inflate(inflater, container, false) return FragmentHomeDetailBinding.inflate(inflater, container, false)
} }
@ -314,6 +341,8 @@ class HomeDetailFragment @Inject constructor(
views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_rooms).render(it.notificationCountRooms, it.notificationHighlightRooms) views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_rooms).render(it.notificationCountRooms, it.notificationHighlightRooms)
views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_notification).render(it.notificationCountCatchup, it.notificationHighlightCatchup) views.bottomNavigationView.getOrCreateBadge(R.id.bottom_action_notification).render(it.notificationCountCatchup, it.notificationHighlightCatchup)
views.syncStateView.render(it.syncState) views.syncStateView.render(it.syncState)
hasUnreadRooms = it.hasUnreadMessages
} }
private fun BadgeDrawable.render(count: Int, highlight: Boolean) { private fun BadgeDrawable.render(count: Int, highlight: Boolean) {

View file

@ -16,22 +16,30 @@
package im.vector.app.features.home package im.vector.app.features.home
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.HasScreenInjector import im.vector.app.core.di.HasScreenInjector
import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.grouplist.SelectedGroupDataSource import im.vector.app.features.grouplist.SelectedGroupDataSource
import im.vector.app.features.ui.UiStateRepository import im.vector.app.features.ui.UiStateRepository
import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.query.RoomCategoryFilter
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.asObservable
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
import timber.log.Timber
import java.util.concurrent.TimeUnit
/** /**
* View model used to update the home bottom bar notification counts, observe the sync state and * View model used to update the home bottom bar notification counts, observe the sync state and
@ -41,7 +49,6 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
private val session: Session, private val session: Session,
private val uiStateRepository: UiStateRepository, private val uiStateRepository: UiStateRepository,
private val selectedGroupStore: SelectedGroupDataSource, private val selectedGroupStore: SelectedGroupDataSource,
private val homeRoomListStore: HomeRoomListDataSource,
private val stringProvider: StringProvider) private val stringProvider: StringProvider)
: VectorViewModel<HomeDetailViewState, HomeDetailAction, EmptyViewEvents>(initialState) { : VectorViewModel<HomeDetailViewState, HomeDetailAction, EmptyViewEvents>(initialState) {
@ -75,6 +82,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
override fun handle(action: HomeDetailAction) { override fun handle(action: HomeDetailAction) {
when (action) { when (action) {
is HomeDetailAction.SwitchDisplayMode -> handleSwitchDisplayMode(action) is HomeDetailAction.SwitchDisplayMode -> handleSwitchDisplayMode(action)
HomeDetailAction.MarkAllRoomsRead -> handleMarkAllRoomsRead()
} }
} }
@ -90,6 +98,26 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************
private fun handleMarkAllRoomsRead() = withState { _ ->
// questionable to use viewmodelscope
viewModelScope.launch(Dispatchers.Default) {
val roomIds = session.getRoomSummaries(
roomSummaryQueryParams {
memberships = listOf(Membership.JOIN)
roomCategoryFilter = RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS
}
)
.map { it.roomId }
try {
awaitCallback<Unit> {
session.markAllAsRead(roomIds, it)
}
} catch (failure: Throwable) {
Timber.d(failure, "Failed to mark all as read")
}
}
}
private fun observeSyncState() { private fun observeSyncState() {
session.rx() session.rx()
.liveSyncState() .liveSyncState()
@ -113,43 +141,51 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
} }
private fun observeRoomSummaries() { private fun observeRoomSummaries() {
homeRoomListStore session.getPagedRoomSummariesLive(
.observe() roomSummaryQueryParams {
.observeOn(Schedulers.computation()) memberships = Membership.activeMemberships()
.map { it.asSequence() } }
.subscribe { summaries -> )
val invitesDm = summaries .asObservable()
.filter { it.membership == Membership.INVITE && it.isDirect } .throttleFirst(300, TimeUnit.MILLISECONDS)
.count() .subscribe {
val dmInvites = session.getRoomSummaries(
roomSummaryQueryParams {
memberships = listOf(Membership.INVITE)
roomCategoryFilter = RoomCategoryFilter.ONLY_DM
}
).size
val invitesRoom = summaries val roomsInvite = session.getRoomSummaries(
.filter { it.membership == Membership.INVITE && it.isDirect.not() } roomSummaryQueryParams {
.count() memberships = listOf(Membership.INVITE)
roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
}
).size
val peopleNotifications = summaries val dmRooms = session.getNotificationCountForRooms(
.filter { it.isDirect } roomSummaryQueryParams {
.map { it.notificationCount } memberships = listOf(Membership.JOIN)
.sum() roomCategoryFilter = RoomCategoryFilter.ONLY_DM
val peopleHasHighlight = summaries }
.filter { it.isDirect } )
.any { it.highlightCount > 0 }
val roomsNotifications = summaries val otherRooms = session.getNotificationCountForRooms(
.filter { !it.isDirect } roomSummaryQueryParams {
.map { it.notificationCount } memberships = listOf(Membership.JOIN)
.sum() roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
val roomsHasHighlight = summaries }
.filter { !it.isDirect } )
.any { it.highlightCount > 0 }
setState { setState {
copy( copy(
notificationCountCatchup = peopleNotifications + roomsNotifications + invitesDm + invitesRoom, notificationCountCatchup = dmRooms.totalCount + otherRooms.totalCount + roomsInvite + dmInvites,
notificationHighlightCatchup = peopleHasHighlight || roomsHasHighlight, notificationHighlightCatchup = dmRooms.isHighlight || otherRooms.isHighlight,
notificationCountPeople = peopleNotifications + invitesDm, notificationCountPeople = dmRooms.totalCount + dmInvites,
notificationHighlightPeople = peopleHasHighlight || invitesDm > 0, notificationHighlightPeople = dmRooms.isHighlight || dmInvites > 0,
notificationCountRooms = roomsNotifications + invitesRoom, notificationCountRooms = otherRooms.totalCount + roomsInvite,
notificationHighlightRooms = roomsHasHighlight || invitesRoom > 0 notificationHighlightRooms = otherRooms.isHighlight || roomsInvite > 0,
hasUnreadMessages = dmRooms.totalCount + otherRooms.totalCount > 0
) )
} }
} }

View file

@ -34,5 +34,6 @@ data class HomeDetailViewState(
val notificationHighlightPeople: Boolean = false, val notificationHighlightPeople: Boolean = false,
val notificationCountRooms: Int = 0, val notificationCountRooms: Int = 0,
val notificationHighlightRooms: Boolean = false, val notificationHighlightRooms: Boolean = false,
val hasUnreadMessages: Boolean = false,
val syncState: SyncState = SyncState.Idle val syncState: SyncState = SyncState.Idle
) : MvRxState ) : MvRxState

View file

@ -21,36 +21,44 @@ import android.content.pm.ShortcutManager
import android.os.Build import android.os.Build
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import io.reactivex.Observable import im.vector.app.core.di.ActiveSessionHolder
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers import io.reactivex.disposables.Disposables
import org.matrix.android.sdk.api.query.RoomTagQueryFilter
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.rx.asObservable
import javax.inject.Inject import javax.inject.Inject
class ShortcutsHandler @Inject constructor( class ShortcutsHandler @Inject constructor(
private val context: Context, private val context: Context,
private val homeRoomListStore: HomeRoomListDataSource, private val shortcutCreator: ShortcutCreator,
private val shortcutCreator: ShortcutCreator private val activeSessionHolder: ActiveSessionHolder
) { ) {
fun observeRoomsAndBuildShortcuts(): Disposable { fun observeRoomsAndBuildShortcuts(): Disposable {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
// No op // No op
return Observable.empty<Unit>().subscribe() return Disposables.empty()
} }
return homeRoomListStore return activeSessionHolder.getSafeActiveSession()
.observe() ?.getPagedRoomSummariesLive(
.distinctUntilChanged() roomSummaryQueryParams {
.observeOn(Schedulers.computation()) memberships = listOf(Membership.JOIN)
.subscribe { rooms -> roomTagQueryFilter = RoomTagQueryFilter(isFavorite = true, null, null)
}
)
?.asObservable()
?.subscribe { rooms ->
val shortcuts = rooms val shortcuts = rooms
.filter { room -> room.isFavorite }
.take(n = 4) // Android only allows us to create 4 shortcuts .take(n = 4) // Android only allows us to create 4 shortcuts
.map { shortcutCreator.create(it) } .map { shortcutCreator.create(it) }
ShortcutManagerCompat.removeAllDynamicShortcuts(context) ShortcutManagerCompat.removeAllDynamicShortcuts(context)
ShortcutManagerCompat.addDynamicShortcuts(context, shortcuts) ShortcutManagerCompat.addDynamicShortcuts(context, shortcuts)
} }
?: Disposables.empty()
} }
fun clearShortcuts() { fun clearShortcuts() {

View file

@ -22,12 +22,11 @@ import org.matrix.android.sdk.api.session.room.notification.RoomNotificationStat
sealed class RoomListAction : VectorViewModelAction { sealed class RoomListAction : VectorViewModelAction {
data class SelectRoom(val roomSummary: RoomSummary) : RoomListAction() data class SelectRoom(val roomSummary: RoomSummary) : RoomListAction()
data class ToggleCategory(val category: RoomCategory) : RoomListAction() data class ToggleSection(val section: RoomsSection) : RoomListAction()
data class AcceptInvitation(val roomSummary: RoomSummary) : RoomListAction() data class AcceptInvitation(val roomSummary: RoomSummary) : RoomListAction()
data class RejectInvitation(val roomSummary: RoomSummary) : RoomListAction() data class RejectInvitation(val roomSummary: RoomSummary) : RoomListAction()
data class FilterWith(val filter: String) : RoomListAction() data class FilterWith(val filter: String) : RoomListAction()
data class ChangeRoomNotificationState(val roomId: String, val notificationState: RoomNotificationState) : RoomListAction() data class ChangeRoomNotificationState(val roomId: String, val notificationState: RoomNotificationState) : RoomListAction()
data class ToggleTag(val roomId: String, val tag: String) : RoomListAction() data class ToggleTag(val roomId: String, val tag: String) : RoomListAction()
data class LeaveRoom(val roomId: String) : RoomListAction() data class LeaveRoom(val roomId: String) : RoomListAction()
object MarkAllRoomsRead : RoomListAction()
} }

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.list
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R
import im.vector.app.core.epoxy.helpFooterItem
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.filtered.filteredRoomFooterItem
import javax.inject.Inject
class RoomListFooterController @Inject constructor(
private val stringProvider: StringProvider,
private val userPreferencesProvider: UserPreferencesProvider
) : TypedEpoxyController<RoomListViewState>() {
var listener: RoomListListener? = null
override fun buildModels(data: RoomListViewState?) {
when (data?.displayMode) {
RoomListDisplayMode.FILTERED -> {
filteredRoomFooterItem {
id("filter_footer")
listener(listener)
currentFilter(data.roomFilter)
}
}
else -> {
if (userPreferencesProvider.shouldShowLongClickOnRoomHelp()) {
helpFooterItem {
id("long_click_help")
text(stringProvider.getString(R.string.help_long_click_on_room_for_more_options))
}
}
}
}
}
}

View file

@ -20,19 +20,15 @@ import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
@ -44,6 +40,7 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.StateView import im.vector.app.core.platform.StateView
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.databinding.FragmentRoomListBinding import im.vector.app.databinding.FragmentRoomListBinding
import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.list.actions.RoomListActionsArgs import im.vector.app.features.home.room.list.actions.RoomListActionsArgs
@ -53,8 +50,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA
import im.vector.app.features.home.room.list.widget.NotifsFabMenuView import im.vector.app.features.home.room.list.widget.NotifsFabMenuView
import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationDrawerManager
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState
@ -66,12 +62,13 @@ data class RoomListParams(
) : Parcelable ) : Parcelable
class RoomListFragment @Inject constructor( class RoomListFragment @Inject constructor(
private val roomController: RoomSummaryController, private val pagedControllerFactory: RoomSummaryPagedControllerFactory,
val roomListViewModelFactory: RoomListViewModel.Factory, val roomListViewModelFactory: RoomListViewModel.Factory,
private val notificationDrawerManager: NotificationDrawerManager, private val notificationDrawerManager: NotificationDrawerManager,
private val sharedViewPool: RecyclerView.RecycledViewPool private val footerController: RoomListFooterController,
private val userPreferencesProvider: UserPreferencesProvider
) : VectorBaseFragment<FragmentRoomListBinding>(), ) : VectorBaseFragment<FragmentRoomListBinding>(),
RoomSummaryController.Listener, RoomListListener,
OnBackPressed, OnBackPressed,
NotifsFabMenuView.Listener { NotifsFabMenuView.Listener {
@ -85,28 +82,25 @@ class RoomListFragment @Inject constructor(
return FragmentRoomListBinding.inflate(inflater, container, false) return FragmentRoomListBinding.inflate(inflater, container, false)
} }
private var hasUnreadRooms = false data class SectionKey(
val name: String,
val isExpanded: Boolean,
val notifyOfLocalEcho: Boolean
)
override fun getMenuRes() = R.menu.room_list data class SectionAdapterInfo(
var section: SectionKey,
val headerHeaderAdapter: SectionHeaderAdapter,
val contentAdapter: RoomSummaryPagedController
)
override fun onOptionsItemSelected(item: MenuItem): Boolean { private val adapterInfosList = mutableListOf<SectionAdapterInfo>()
when (item.itemId) { private var concatAdapter : ConcatAdapter? = null
R.id.menu_home_mark_all_as_read -> {
roomListViewModel.handle(RoomListAction.MarkAllRoomsRead)
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onPrepareOptionsMenu(menu: Menu) {
menu.findItem(R.id.menu_home_mark_all_as_read).isVisible = hasUnreadRooms
super.onPrepareOptionsMenu(menu)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
views.stateView.contentView = views.roomListView
views.stateView.state = StateView.State.Loading
setupCreateRoomButton() setupCreateRoomButton()
setupRecyclerView() setupRecyclerView()
sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
@ -125,6 +119,40 @@ class RoomListFragment @Inject constructor(
.observe() .observe()
.subscribe { handleQuickActions(it) } .subscribe { handleQuickActions(it) }
.disposeOnDestroyView() .disposeOnDestroyView()
roomListViewModel.selectSubscribe(viewLifecycleOwner, RoomListViewState::roomMembershipChanges) { ms ->
// it's for invites local echo
adapterInfosList.filter { it.section.notifyOfLocalEcho }
.onEach {
it.contentAdapter.roomChangeMembershipStates = ms
}
}
}
private fun refreshCollapseStates() {
var contentInsertIndex = 1
roomListViewModel.sections.forEachIndexed { index, roomsSection ->
val actualBlock = adapterInfosList[index]
val isRoomSectionExpanded = roomsSection.isExpanded.value.orTrue()
if (actualBlock.section.isExpanded && !isRoomSectionExpanded) {
// we have to remove the content adapter
concatAdapter?.removeAdapter(actualBlock.contentAdapter.adapter)
} else if (!actualBlock.section.isExpanded && isRoomSectionExpanded) {
// we must add it back!
concatAdapter?.addAdapter(contentInsertIndex, actualBlock.contentAdapter.adapter)
}
contentInsertIndex = if (isRoomSectionExpanded) {
contentInsertIndex + 2
} else {
contentInsertIndex + 1
}
actualBlock.section = actualBlock.section.copy(
isExpanded = isRoomSectionExpanded
)
actualBlock.headerHeaderAdapter.updateSection(
actualBlock.headerHeaderAdapter.roomsSectionData.copy(isExpanded = isRoomSectionExpanded)
)
}
} }
override fun showFailure(throwable: Throwable) { override fun showFailure(throwable: Throwable) {
@ -132,12 +160,15 @@ class RoomListFragment @Inject constructor(
} }
override fun onDestroyView() { override fun onDestroyView() {
roomController.removeModelBuildListener(modelBuildListener) adapterInfosList.onEach { it.contentAdapter.removeModelBuildListener(modelBuildListener) }
adapterInfosList.clear()
modelBuildListener = null modelBuildListener = null
views.roomListView.cleanup() views.roomListView.cleanup()
roomController.listener = null footerController.listener = null
// TODO Cleanup listener on the ConcatAdapter's adapters?
stateRestorer.clear() stateRestorer.clear()
views.createChatFabMenu.listener = null views.createChatFabMenu.listener = null
concatAdapter = null
super.onDestroyView() super.onDestroyView()
} }
@ -204,13 +235,58 @@ class RoomListFragment @Inject constructor(
stateRestorer = LayoutManagerStateRestorer(layoutManager).register() stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
views.roomListView.layoutManager = layoutManager views.roomListView.layoutManager = layoutManager
views.roomListView.itemAnimator = RoomListAnimator() views.roomListView.itemAnimator = RoomListAnimator()
views.roomListView.setRecycledViewPool(sharedViewPool)
layoutManager.recycleChildrenOnDetach = true layoutManager.recycleChildrenOnDetach = true
roomController.listener = this
modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) } modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) }
roomController.addModelBuildListener(modelBuildListener)
views.roomListView.adapter = roomController.adapter val concatAdapter = ConcatAdapter()
views.stateView.contentView = views.roomListView
roomListViewModel.sections.forEach { section ->
val sectionAdapter = SectionHeaderAdapter {
roomListViewModel.handle(RoomListAction.ToggleSection(section))
}.also {
it.updateSection(SectionHeaderAdapter.RoomsSectionData(section.sectionName))
}
val contentAdapter = pagedControllerFactory.createRoomSummaryPagedController()
.also { controller ->
section.livePages.observe(viewLifecycleOwner) { pl ->
controller.submitList(pl)
sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy(isHidden = pl.isEmpty()))
checkEmptyState()
}
section.notificationCount.observe(viewLifecycleOwner) { counts ->
sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy(
notificationCount = counts.totalCount,
isHighlighted = counts.isHighlight
))
}
section.isExpanded.observe(viewLifecycleOwner) { _ ->
refreshCollapseStates()
}
controller.listener = this
}
adapterInfosList.add(
SectionAdapterInfo(
SectionKey(
name = section.sectionName,
isExpanded = section.isExpanded.value.orTrue(),
notifyOfLocalEcho = section.notifyOfLocalEcho
),
sectionAdapter,
contentAdapter
)
)
concatAdapter.addAdapter(sectionAdapter)
concatAdapter.addAdapter(contentAdapter.adapter)
}
// Add the footer controller
footerController.listener = this
concatAdapter.addAdapter(footerController.adapter)
this.concatAdapter = concatAdapter
views.roomListView.adapter = concatAdapter
} }
private val showFabRunnable = Runnable { private val showFabRunnable = Runnable {
@ -278,89 +354,41 @@ class RoomListFragment @Inject constructor(
} }
override fun invalidate() = withState(roomListViewModel) { state -> override fun invalidate() = withState(roomListViewModel) { state ->
when (state.asyncFilteredRooms) { footerController.setData(state)
is Incomplete -> renderLoading()
is Success -> renderSuccess(state)
is Fail -> renderFailure(state.asyncFilteredRooms.error)
}
roomController.update(state)
// Mark all as read menu
when (roomListParams.displayMode) {
RoomListDisplayMode.NOTIFICATIONS,
RoomListDisplayMode.PEOPLE,
RoomListDisplayMode.ROOMS -> {
val newValue = state.hasUnread
if (hasUnreadRooms != newValue) {
hasUnreadRooms = newValue
invalidateOptionsMenu()
}
}
else -> Unit
}
} }
private fun renderSuccess(state: RoomListViewState) { private fun checkEmptyState() {
val allRooms = state.asyncRooms() val hasNoRoom = adapterInfosList.all { it.headerHeaderAdapter.roomsSectionData.isHidden }
val filteredRooms = state.asyncFilteredRooms() if (hasNoRoom) {
if (filteredRooms.isNullOrEmpty()) { val emptyState = when (roomListParams.displayMode) {
renderEmptyState(allRooms) RoomListDisplayMode.NOTIFICATIONS -> {
} else {
views.stateView.state = StateView.State.Content
}
}
private fun renderEmptyState(allRooms: List<RoomSummary>?) {
val hasNoRoom = allRooms
?.filter {
it.membership == Membership.JOIN || it.membership == Membership.INVITE
}
.isNullOrEmpty()
val emptyState = when (roomListParams.displayMode) {
RoomListDisplayMode.NOTIFICATIONS -> {
if (hasNoRoom) {
StateView.State.Empty(
title = getString(R.string.room_list_catchup_welcome_title),
image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_catchup),
message = getString(R.string.room_list_catchup_welcome_body)
)
} else {
StateView.State.Empty( StateView.State.Empty(
title = getString(R.string.room_list_catchup_empty_title), title = getString(R.string.room_list_catchup_empty_title),
image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_noun_party_popper), image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_noun_party_popper),
message = getString(R.string.room_list_catchup_empty_body)) message = getString(R.string.room_list_catchup_empty_body))
} }
RoomListDisplayMode.PEOPLE ->
StateView.State.Empty(
title = getString(R.string.room_list_people_empty_title),
image = ContextCompat.getDrawable(requireContext(), R.drawable.empty_state_dm),
isBigImage = true,
message = getString(R.string.room_list_people_empty_body)
)
RoomListDisplayMode.ROOMS ->
StateView.State.Empty(
title = getString(R.string.room_list_rooms_empty_title),
image = ContextCompat.getDrawable(requireContext(), R.drawable.empty_state_room),
isBigImage = true,
message = getString(R.string.room_list_rooms_empty_body)
)
else ->
// Always display the content in this mode, because if the footer
StateView.State.Content
} }
RoomListDisplayMode.PEOPLE -> views.stateView.state = emptyState
StateView.State.Empty( } else {
title = getString(R.string.room_list_people_empty_title), views.stateView.state = StateView.State.Content
image = ContextCompat.getDrawable(requireContext(), R.drawable.empty_state_dm),
isBigImage = true,
message = getString(R.string.room_list_people_empty_body)
)
RoomListDisplayMode.ROOMS ->
StateView.State.Empty(
title = getString(R.string.room_list_rooms_empty_title),
image = ContextCompat.getDrawable(requireContext(), R.drawable.empty_state_room),
isBigImage = true,
message = getString(R.string.room_list_rooms_empty_body)
)
else ->
// Always display the content in this mode, because if the footer
StateView.State.Content
} }
views.stateView.state = emptyState
}
private fun renderLoading() {
views.stateView.state = StateView.State.Loading
}
private fun renderFailure(error: Throwable) {
val message = when (error) {
is Failure.NetworkConnection -> getString(R.string.network_error_please_check_and_retry)
else -> getString(R.string.unknown_error)
}
views.stateView.state = StateView.State.Error(message)
} }
override fun onBackPressed(toolbarButton: Boolean): Boolean { override fun onBackPressed(toolbarButton: Boolean): Boolean {
@ -377,7 +405,11 @@ class RoomListFragment @Inject constructor(
} }
override fun onRoomLongClicked(room: RoomSummary): Boolean { override fun onRoomLongClicked(room: RoomSummary): Boolean {
roomController.onRoomLongClicked() userPreferencesProvider.neverShowLongClickOnRoomHelpAgain()
withState(roomListViewModel) {
// refresh footer
footerController.setData(it)
}
RoomListQuickActionsBottomSheet RoomListQuickActionsBottomSheet
.newInstance(room.roomId, RoomListActionsArgs.Mode.FULL) .newInstance(room.roomId, RoomListActionsArgs.Mode.FULL)
.show(childFragmentManager, "ROOM_LIST_QUICK_ACTIONS") .show(childFragmentManager, "ROOM_LIST_QUICK_ACTIONS")
@ -394,10 +426,6 @@ class RoomListFragment @Inject constructor(
roomListViewModel.handle(RoomListAction.RejectInvitation(room)) roomListViewModel.handle(RoomListAction.RejectInvitation(room))
} }
override fun onToggleRoomCategory(roomCategory: RoomCategory) {
roomListViewModel.handle(RoomListAction.ToggleCategory(roomCategory))
}
override fun createRoom(initialName: String) { override fun createRoom(initialName: String) {
navigator.openCreateRoom(requireActivity(), initialName) navigator.openCreateRoom(requireActivity(), initialName)
} }

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.list
import im.vector.app.features.home.room.filtered.FilteredRoomFooterItem
import org.matrix.android.sdk.api.session.room.model.RoomSummary
interface RoomListListener : FilteredRoomFooterItem.FilteredRoomFooterItemListener {
fun onRoomClicked(room: RoomSummary)
fun onRoomLongClicked(room: RoomSummary): Boolean
fun onRejectRoomInvitation(room: RoomSummary)
fun onAcceptRoomInvitation(room: RoomSummary)
}

View file

@ -16,37 +16,61 @@
package im.vector.app.features.home.room.list package im.vector.app.features.home.room.list
import androidx.annotation.StringRes
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import im.vector.app.R
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.utils.DataSource import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.RoomListDisplayMode
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.query.RoomCategoryFilter
import org.matrix.android.sdk.api.query.RoomTagQueryFilter
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.UpdatableFilterLivePageResult
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.state.isPublic import org.matrix.android.sdk.api.session.room.state.isPublic
import org.matrix.android.sdk.rx.asObservable
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
import timber.log.Timber import timber.log.Timber
import java.lang.Exception
import javax.inject.Inject import javax.inject.Inject
class RoomListViewModel @Inject constructor(initialState: RoomListViewState, class RoomListViewModel @Inject constructor(
private val session: Session, initialState: RoomListViewState,
private val roomSummariesSource: DataSource<List<RoomSummary>>) private val session: Session,
: VectorViewModel<RoomListViewState, RoomListAction, RoomListViewEvents>(initialState) { private val stringProvider: StringProvider
) : VectorViewModel<RoomListViewState, RoomListAction, RoomListViewEvents>(initialState) {
interface Factory { interface Factory {
fun create(initialState: RoomListViewState): RoomListViewModel fun create(initialState: RoomListViewState): RoomListViewModel
} }
private var updatableQuery: UpdatableFilterLivePageResult? = null
init {
observeMembershipChanges()
}
private fun observeMembershipChanges() {
session.rx()
.liveRoomChangeMembershipState()
.subscribe {
setState { copy(roomMembershipChanges = it) }
}
.disposeOnClear()
}
companion object : MvRxViewModelFactory<RoomListViewModel, RoomListViewState> { companion object : MvRxViewModelFactory<RoomListViewModel, RoomListViewState> {
@JvmStatic @JvmStatic
@ -56,28 +80,136 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
} }
} }
private val displayMode = initialState.displayMode val sections: List<RoomsSection> by lazy {
private val roomListDisplayModeFilter = RoomListDisplayModeFilter(displayMode) val sections = mutableListOf<RoomsSection>()
if (initialState.displayMode == RoomListDisplayMode.PEOPLE) {
addSection(sections, R.string.invitations_header, true) {
it.memberships = listOf(Membership.INVITE)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
}
init { addSection(sections, R.string.bottom_action_favourites) {
observeRoomSummaries() it.memberships = listOf(Membership.JOIN)
observeMembershipChanges() it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
it.roomTagQueryFilter = RoomTagQueryFilter(true, null, null)
}
addSection(sections, R.string.bottom_action_people_x) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
}
} else if (initialState.displayMode == RoomListDisplayMode.ROOMS) {
addSection(sections, R.string.invitations_header, true) {
it.memberships = listOf(Membership.INVITE)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
}
addSection(sections, R.string.bottom_action_favourites) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
it.roomTagQueryFilter = RoomTagQueryFilter(true, null, null)
}
addSection(sections, R.string.bottom_action_rooms) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
it.roomTagQueryFilter = RoomTagQueryFilter(false, false, false)
}
addSection(sections, R.string.low_priority_header) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
it.roomTagQueryFilter = RoomTagQueryFilter(null, true, null)
}
addSection(sections, R.string.system_alerts_header) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
it.roomTagQueryFilter = RoomTagQueryFilter(null, null, true)
}
} else if (initialState.displayMode == RoomListDisplayMode.FILTERED) {
withQueryParams(
{
it.memberships = Membership.activeMemberships()
},
{ qpm ->
val name = stringProvider.getString(R.string.bottom_action_rooms)
session.getFilteredPagedRoomSummariesLive(qpm)
.let { updatableFilterLivePageResult ->
updatableQuery = updatableFilterLivePageResult
sections.add(RoomsSection(name, updatableFilterLivePageResult.livePagedList))
}
}
)
} else if (initialState.displayMode == RoomListDisplayMode.NOTIFICATIONS) {
addSection(sections, R.string.invitations_header, true) {
it.memberships = listOf(Membership.INVITE)
it.roomCategoryFilter = RoomCategoryFilter.ALL
}
addSection(sections, R.string.bottom_action_rooms, true) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS
}
}
sections
} }
override fun handle(action: RoomListAction) { override fun handle(action: RoomListAction) {
when (action) { when (action) {
is RoomListAction.SelectRoom -> handleSelectRoom(action) is RoomListAction.SelectRoom -> handleSelectRoom(action)
is RoomListAction.ToggleCategory -> handleToggleCategory(action)
is RoomListAction.AcceptInvitation -> handleAcceptInvitation(action) is RoomListAction.AcceptInvitation -> handleAcceptInvitation(action)
is RoomListAction.RejectInvitation -> handleRejectInvitation(action) is RoomListAction.RejectInvitation -> handleRejectInvitation(action)
is RoomListAction.FilterWith -> handleFilter(action) is RoomListAction.FilterWith -> handleFilter(action)
is RoomListAction.MarkAllRoomsRead -> handleMarkAllRoomsRead()
is RoomListAction.LeaveRoom -> handleLeaveRoom(action) is RoomListAction.LeaveRoom -> handleLeaveRoom(action)
is RoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) is RoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
is RoomListAction.ToggleTag -> handleToggleTag(action) is RoomListAction.ToggleTag -> handleToggleTag(action)
is RoomListAction.ToggleSection -> handleToggleSection(action.section)
}.exhaustive }.exhaustive
} }
private fun addSection(sections: MutableList<RoomsSection>,
@StringRes nameRes: Int,
notifyOfLocalEcho: Boolean = false,
query: (RoomSummaryQueryParams.Builder) -> Unit) {
withQueryParams(
{ query.invoke(it) },
{ roomQueryParams ->
val name = stringProvider.getString(nameRes)
session.getPagedRoomSummariesLive(roomQueryParams)
.let { livePagedList ->
// use it also as a source to update count
livePagedList.asObservable()
.observeOn(Schedulers.computation())
.subscribe {
sections.find { it.sectionName == name }
?.notificationCount
?.postValue(session.getNotificationCountForRooms(roomQueryParams))
}
.disposeOnClear()
sections.add(
RoomsSection(
sectionName = name,
livePages = livePagedList,
notifyOfLocalEcho = notifyOfLocalEcho
)
)
}
}
)
}
private fun withQueryParams(builder: (RoomSummaryQueryParams.Builder) -> Unit, block: (RoomSummaryQueryParams) -> Unit) {
RoomSummaryQueryParams.Builder()
.apply { builder.invoke(this) }
.build()
.let { block(it) }
}
fun isPublicRoom(roomId: String): Boolean { fun isPublicRoom(roomId: String): Boolean {
return session.getRoom(roomId)?.isPublic().orFalse() return session.getRoom(roomId)?.isPublic().orFalse()
} }
@ -88,8 +220,14 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
_viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary)) _viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary))
} }
private fun handleToggleCategory(action: RoomListAction.ToggleCategory) = setState { private fun handleToggleSection(roomSection: RoomsSection) {
this.toggle(action.category) roomSection.isExpanded.postValue(!roomSection.isExpanded.value.orFalse())
/* TODO Cleanup if it is working
sections.find { it.sectionName == roomSection.sectionName }
?.let { section ->
section.isExpanded.postValue(!section.isExpanded.value.orFalse())
}
*/
} }
private fun handleFilter(action: RoomListAction.FilterWith) { private fun handleFilter(action: RoomListAction.FilterWith) {
@ -98,23 +236,12 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
roomFilter = action.filter roomFilter = action.filter
) )
} }
} updatableQuery?.updateQuery(
roomSummaryQueryParams {
private fun observeRoomSummaries() { memberships = Membership.activeMemberships()
roomSummariesSource displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.INSENSITIVE)
.observe()
.observeOn(Schedulers.computation())
.execute { asyncRooms ->
copy(asyncRooms = asyncRooms)
}
roomSummariesSource
.observe()
.observeOn(Schedulers.computation())
.map { buildRoomSummaries(it) }
.execute { async ->
copy(asyncFilteredRooms = async)
} }
)
} }
private fun handleAcceptInvitation(action: RoomListAction.AcceptInvitation) = withState { state -> private fun handleAcceptInvitation(action: RoomListAction.AcceptInvitation) = withState { state ->
@ -126,6 +253,19 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
return@withState return@withState
} }
// quick echo
setState {
copy(
roomMembershipChanges = roomMembershipChanges.mapValues {
if (it.key == roomId) {
ChangeMembershipState.Joining
} else {
it.value
}
}
)
}
val room = session.getRoom(roomId) ?: return@withState val room = session.getRoom(roomId) ?: return@withState
viewModelScope.launch { viewModelScope.launch {
try { try {
@ -163,15 +303,6 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
} }
} }
private fun handleMarkAllRoomsRead() = withState { state ->
state.asyncFilteredRooms.invoke()
?.flatMap { it.value }
?.filter { it.membership == Membership.JOIN }
?.map { it.roomId }
?.toList()
?.let { session.markAllAsRead(it, NoOpMatrixCallback()) }
}
private fun handleChangeNotificationMode(action: RoomListAction.ChangeRoomNotificationState) { private fun handleChangeNotificationMode(action: RoomListAction.ChangeRoomNotificationState) {
val room = session.getRoom(action.roomId) val room = session.getRoom(action.roomId)
if (room != null) { if (room != null) {
@ -226,46 +357,4 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
_viewEvents.post(value) _viewEvents.post(value)
} }
} }
private fun observeMembershipChanges() {
session.rx()
.liveRoomChangeMembershipState()
.subscribe {
Timber.v("ChangeMembership states: $it")
setState { copy(roomMembershipChanges = it) }
}
.disposeOnClear()
}
private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
// Set up init size on directChats and groupRooms as they are the biggest ones
val invites = ArrayList<RoomSummary>()
val favourites = ArrayList<RoomSummary>()
val directChats = ArrayList<RoomSummary>(rooms.size)
val groupRooms = ArrayList<RoomSummary>(rooms.size)
val lowPriorities = ArrayList<RoomSummary>()
val serverNotices = ArrayList<RoomSummary>()
rooms
.filter { roomListDisplayModeFilter.test(it) }
.forEach { room ->
val tags = room.tags.map { it.name }
when {
room.membership == Membership.INVITE -> invites.add(room)
tags.contains(RoomTag.ROOM_TAG_SERVER_NOTICE) -> serverNotices.add(room)
tags.contains(RoomTag.ROOM_TAG_FAVOURITE) -> favourites.add(room)
tags.contains(RoomTag.ROOM_TAG_LOW_PRIORITY) -> lowPriorities.add(room)
room.isDirect -> directChats.add(room)
else -> groupRooms.add(room)
}
}
return RoomSummaries().apply {
put(RoomCategory.INVITE, invites)
put(RoomCategory.FAVOURITE, favourites)
put(RoomCategory.DIRECT, directChats)
put(RoomCategory.GROUP, groupRooms)
put(RoomCategory.LOW_PRIORITY, lowPriorities)
put(RoomCategory.SERVER_NOTICE, serverNotices)
}
}
} }

View file

@ -16,20 +16,20 @@
package im.vector.app.features.home.room.list package im.vector.app.features.home.room.list
import im.vector.app.features.home.HomeRoomListDataSource import im.vector.app.core.resources.StringProvider
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
class RoomListViewModelFactory @Inject constructor(private val session: Provider<Session>, class RoomListViewModelFactory @Inject constructor(private val session: Provider<Session>,
private val homeRoomListDataSource: Provider<HomeRoomListDataSource>) private val stringProvider: StringProvider)
: RoomListViewModel.Factory { : RoomListViewModel.Factory {
override fun create(initialState: RoomListViewState): RoomListViewModel { override fun create(initialState: RoomListViewState): RoomListViewModel {
return RoomListViewModel( return RoomListViewModel(
initialState, initialState,
session.get(), session.get(),
homeRoomListDataSource.get() stringProvider
) )
} }
} }

View file

@ -16,73 +16,15 @@
package im.vector.app.features.home.room.list package im.vector.app.features.home.room.list
import androidx.annotation.StringRes
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.app.R
import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.RoomListDisplayMode
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
data class RoomListViewState( data class RoomListViewState(
val displayMode: RoomListDisplayMode, val displayMode: RoomListDisplayMode,
val asyncRooms: Async<List<RoomSummary>> = Uninitialized,
val roomFilter: String = "", val roomFilter: String = "",
val asyncFilteredRooms: Async<RoomSummaries> = Uninitialized, val roomMembershipChanges: Map<String, ChangeMembershipState> = emptyMap()
val roomMembershipChanges: Map<String, ChangeMembershipState> = emptyMap(),
val isInviteExpanded: Boolean = true,
val isFavouriteRoomsExpanded: Boolean = true,
val isDirectRoomsExpanded: Boolean = true,
val isGroupRoomsExpanded: Boolean = true,
val isLowPriorityRoomsExpanded: Boolean = true,
val isServerNoticeRoomsExpanded: Boolean = true
) : MvRxState { ) : MvRxState {
constructor(args: RoomListParams) : this(displayMode = args.displayMode) constructor(args: RoomListParams) : this(displayMode = args.displayMode)
fun isCategoryExpanded(roomCategory: RoomCategory): Boolean {
return when (roomCategory) {
RoomCategory.INVITE -> isInviteExpanded
RoomCategory.FAVOURITE -> isFavouriteRoomsExpanded
RoomCategory.DIRECT -> isDirectRoomsExpanded
RoomCategory.GROUP -> isGroupRoomsExpanded
RoomCategory.LOW_PRIORITY -> isLowPriorityRoomsExpanded
RoomCategory.SERVER_NOTICE -> isServerNoticeRoomsExpanded
}
}
fun toggle(roomCategory: RoomCategory): RoomListViewState {
return when (roomCategory) {
RoomCategory.INVITE -> copy(isInviteExpanded = !isInviteExpanded)
RoomCategory.FAVOURITE -> copy(isFavouriteRoomsExpanded = !isFavouriteRoomsExpanded)
RoomCategory.DIRECT -> copy(isDirectRoomsExpanded = !isDirectRoomsExpanded)
RoomCategory.GROUP -> copy(isGroupRoomsExpanded = !isGroupRoomsExpanded)
RoomCategory.LOW_PRIORITY -> copy(isLowPriorityRoomsExpanded = !isLowPriorityRoomsExpanded)
RoomCategory.SERVER_NOTICE -> copy(isServerNoticeRoomsExpanded = !isServerNoticeRoomsExpanded)
}
}
val hasUnread: Boolean
get() = asyncFilteredRooms.invoke()
?.flatMap { it.value }
?.filter { it.membership == Membership.JOIN }
?.any { it.hasUnreadMessages }
?: false
}
typealias RoomSummaries = LinkedHashMap<RoomCategory, List<RoomSummary>>
enum class RoomCategory(@StringRes val titleRes: Int) {
INVITE(R.string.invitations_header),
FAVOURITE(R.string.bottom_action_favourites),
DIRECT(R.string.bottom_action_people_x),
GROUP(R.string.bottom_action_rooms),
LOW_PRIORITY(R.string.low_priority_header),
SERVER_NOTICE(R.string.system_alerts_header)
}
fun RoomSummaries?.isNullOrEmpty(): Boolean {
return this == null || this.values.flatten().isEmpty()
} }

View file

@ -1,170 +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.app.features.home.room.list
import androidx.annotation.StringRes
import com.airbnb.epoxy.EpoxyController
import im.vector.app.R
import im.vector.app.core.epoxy.helpFooterItem
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.filtered.FilteredRoomFooterItem
import im.vector.app.features.home.room.filtered.filteredRoomFooterItem
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import javax.inject.Inject
class RoomSummaryController @Inject constructor(private val stringProvider: StringProvider,
private val roomSummaryItemFactory: RoomSummaryItemFactory,
private val roomListNameFilter: RoomListNameFilter,
private val userPreferencesProvider: UserPreferencesProvider
) : EpoxyController() {
var listener: Listener? = null
private var viewState: RoomListViewState? = null
init {
// We are requesting a model build directly as the first build of epoxy is on the main thread.
// It avoids to build the whole list of rooms on the main thread.
requestModelBuild()
}
fun update(viewState: RoomListViewState) {
this.viewState = viewState
requestModelBuild()
}
fun onRoomLongClicked() {
userPreferencesProvider.neverShowLongClickOnRoomHelpAgain()
requestModelBuild()
}
override fun buildModels() {
val nonNullViewState = viewState ?: return
when (nonNullViewState.displayMode) {
RoomListDisplayMode.FILTERED -> buildFilteredRooms(nonNullViewState)
else -> buildRooms(nonNullViewState)
}
}
private fun buildFilteredRooms(viewState: RoomListViewState) {
val summaries = viewState.asyncRooms() ?: return
roomListNameFilter.filter = viewState.roomFilter
val filteredSummaries = summaries
.filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) }
buildRoomModels(filteredSummaries,
viewState.roomMembershipChanges,
emptySet())
addFilterFooter(viewState)
}
private fun buildRooms(viewState: RoomListViewState) {
var showHelp = false
val roomSummaries = viewState.asyncFilteredRooms()
roomSummaries?.forEach { (category, summaries) ->
if (summaries.isEmpty()) {
return@forEach
} else {
val isExpanded = viewState.isCategoryExpanded(category)
buildRoomCategory(viewState, summaries, category.titleRes, viewState.isCategoryExpanded(category)) {
listener?.onToggleRoomCategory(category)
}
if (isExpanded) {
buildRoomModels(summaries,
viewState.roomMembershipChanges,
emptySet())
// Never set showHelp to true for invitation
if (category != RoomCategory.INVITE) {
showHelp = userPreferencesProvider.shouldShowLongClickOnRoomHelp()
}
}
}
}
if (showHelp) {
buildLongClickHelp()
}
}
private fun buildLongClickHelp() {
helpFooterItem {
id("long_click_help")
text(stringProvider.getString(R.string.help_long_click_on_room_for_more_options))
}
}
private fun addFilterFooter(viewState: RoomListViewState) {
filteredRoomFooterItem {
id("filter_footer")
listener(listener)
currentFilter(viewState.roomFilter)
}
}
private fun buildRoomCategory(viewState: RoomListViewState,
summaries: List<RoomSummary>,
@StringRes titleRes: Int,
isExpanded: Boolean,
mutateExpandedState: () -> Unit) {
// TODO should add some business logic later
val unreadCount = if (summaries.isEmpty()) {
0
} else {
summaries.map { it.notificationCount }.sumBy { i -> i }
}
val showHighlighted = summaries.any { it.highlightCount > 0 }
roomCategoryItem {
id(titleRes)
title(stringProvider.getString(titleRes))
expanded(isExpanded)
unreadNotificationCount(unreadCount)
showHighlighted(showHighlighted)
listener {
mutateExpandedState()
update(viewState)
}
}
}
private fun buildRoomModels(summaries: List<RoomSummary>,
roomChangedMembershipStates: Map<String, ChangeMembershipState>,
selectedRoomIds: Set<String>) {
summaries.forEach { roomSummary ->
roomSummaryItemFactory
.create(roomSummary,
roomChangedMembershipStates,
selectedRoomIds,
listener)
.addTo(this)
}
}
interface Listener : FilteredRoomFooterItem.FilteredRoomFooterItemListener {
fun onToggleRoomCategory(roomCategory: RoomCategory)
fun onRoomClicked(room: RoomSummary)
fun onRoomLongClicked(room: RoomSummary): Boolean
fun onRejectRoomInvitation(room: RoomSummary)
fun onAcceptRoomInvitation(room: RoomSummary)
}
}

View file

@ -40,7 +40,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
fun create(roomSummary: RoomSummary, fun create(roomSummary: RoomSummary,
roomChangeMembershipStates: Map<String, ChangeMembershipState>, roomChangeMembershipStates: Map<String, ChangeMembershipState>,
selectedRoomIds: Set<String>, selectedRoomIds: Set<String>,
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> { listener: RoomListListener?): VectorEpoxyModel<*> {
return when (roomSummary.membership) { return when (roomSummary.membership) {
Membership.INVITE -> { Membership.INVITE -> {
val changeMembershipState = roomChangeMembershipStates[roomSummary.roomId] ?: ChangeMembershipState.Unknown val changeMembershipState = roomChangeMembershipStates[roomSummary.roomId] ?: ChangeMembershipState.Unknown
@ -52,7 +52,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
private fun createInvitationItem(roomSummary: RoomSummary, private fun createInvitationItem(roomSummary: RoomSummary,
changeMembershipState: ChangeMembershipState, changeMembershipState: ChangeMembershipState,
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> { listener: RoomListListener?): VectorEpoxyModel<*> {
val secondLine = if (roomSummary.isDirect) { val secondLine = if (roomSummary.isDirect) {
roomSummary.inviterId roomSummary.inviterId
} else { } else {

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.list
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController
import im.vector.app.core.utils.createUIHandler
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import javax.inject.Inject
class RoomSummaryPagedControllerFactory @Inject constructor(
private val roomSummaryItemFactory: RoomSummaryItemFactory
) {
fun createRoomSummaryPagedController(): RoomSummaryPagedController {
return RoomSummaryPagedController(roomSummaryItemFactory)
}
}
class RoomSummaryPagedController(
private val roomSummaryItemFactory: RoomSummaryItemFactory
) : PagedListEpoxyController<RoomSummary>(
// Important it must match the PageList builder notify Looper
modelBuildingHandler = createUIHandler()
) {
var listener: RoomListListener? = null
var roomChangeMembershipStates: Map<String, ChangeMembershipState>? = null
set(value) {
field = value
// ideally we could search for visible models and update only those
requestForcedModelBuild()
}
override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> {
// for place holder if enabled
item ?: return roomSummaryItemFactory.createRoomItem(
roomSummary = RoomSummary(
roomId = "null_item_pos_$currentPosition",
name = "",
encryptionEventTs = null,
isEncrypted = false,
typingUsers = emptyList()
),
selectedRoomIds = emptySet(),
onClick = null,
onLongClick = null
)
return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), listener)
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.list
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.paging.PagedList
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
data class RoomsSection(
val sectionName: String,
val livePages: LiveData<PagedList<RoomSummary>>,
val isExpanded: MutableLiveData<Boolean> = MutableLiveData(true),
val notificationCount: MutableLiveData<RoomAggregateNotificationCount> = MutableLiveData(RoomAggregateNotificationCount(0, 0)),
val notifyOfLocalEcho: Boolean = false
)

View file

@ -0,0 +1,100 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.list
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.recyclerview.widget.RecyclerView
import im.vector.app.R
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.databinding.ItemRoomCategoryBinding
import im.vector.app.features.themes.ThemeUtils
class SectionHeaderAdapter constructor(
private val onClickAction: (() -> Unit)
) : RecyclerView.Adapter<SectionHeaderAdapter.VH>() {
data class RoomsSectionData(
val name: String,
val isExpanded: Boolean = true,
val notificationCount: Int = 0,
val isHighlighted: Boolean = false,
val isHidden: Boolean = true
)
lateinit var roomsSectionData: RoomsSectionData
private set
fun updateSection(newRoomsSectionData: RoomsSectionData) {
if (!::roomsSectionData.isInitialized || newRoomsSectionData != roomsSectionData) {
roomsSectionData = newRoomsSectionData
notifyDataSetChanged()
}
}
init {
setHasStableIds(true)
}
override fun getItemId(position: Int) = roomsSectionData.hashCode().toLong()
override fun getItemViewType(position: Int) = R.layout.item_room_category
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
return VH.create(parent, this.onClickAction)
}
override fun onBindViewHolder(holder: VH, position: Int) {
holder.bind(roomsSectionData)
}
override fun getItemCount(): Int = if (roomsSectionData.isHidden) 0 else 1
class VH constructor(
private val binding: ItemRoomCategoryBinding,
onClickAction: (() -> Unit)
) : RecyclerView.ViewHolder(binding.root) {
init {
binding.root.setOnClickListener(DebouncedClickListener({
onClickAction.invoke()
}))
}
fun bind(roomsSectionData: RoomsSectionData) {
binding.roomCategoryTitleView.text = roomsSectionData.name
val tintColor = ThemeUtils.getColor(binding.root.context, R.attr.riotx_text_secondary)
val expandedArrowDrawableRes = if (roomsSectionData.isExpanded) R.drawable.ic_expand_more_white else R.drawable.ic_expand_less_white
val expandedArrowDrawable = ContextCompat.getDrawable(binding.root.context, expandedArrowDrawableRes)?.also {
DrawableCompat.setTint(it, tintColor)
}
binding.roomCategoryUnreadCounterBadgeView.render(UnreadCounterBadgeView.State(roomsSectionData.notificationCount, roomsSectionData.isHighlighted))
binding.roomCategoryTitleView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null)
}
companion object {
fun create(parent: ViewGroup, onClickAction: () -> Unit): VH {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_room_category, parent, false)
val binding = ItemRoomCategoryBinding.bind(view)
return VH(binding, onClickAction)
}
}
}
}