diff --git a/CHANGES.md b/CHANGES.md index d416f30f26..a354d4b9e4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ Improvements 🙌: - Update reactions to Unicode 13.1 (#2998) - 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) + - Room list improvements (paging) Bugfix 🐛: - Fix bad theme change for the MainActivity diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomCategoryFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomCategoryFilter.kt new file mode 100644 index 0000000000..c8ccc4c8a3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomCategoryFilter.kt @@ -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 +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomTagQueryFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomTagQueryFilter.kt new file mode 100644 index 0000000000..613916bc18 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/RoomTagQueryFilter.kt @@ -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? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt index 5f02b77a1e..8c833644ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session.room import androidx.lifecycle.LiveData +import androidx.paging.PagedList import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.events.model.Event 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.create.CreateRoomParams 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.Optional 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 */ fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback) + + /** + * TODO Doc + */ + fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, + pagedListConfig: PagedList.Config = defaultPagedListConfig): LiveData> + + /** + * 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() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt index f859d74a6f..7e04ebb5f2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt @@ -17,6 +17,8 @@ package org.matrix.android.sdk.api.session.room 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 fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams { @@ -31,7 +33,9 @@ data class RoomSummaryQueryParams( val roomId: QueryStringValue, val displayName: QueryStringValue, val canonicalAlias: QueryStringValue, - val memberships: List + val memberships: List, + val roomCategoryFilter: RoomCategoryFilter?, + val roomTagQueryFilter: RoomTagQueryFilter? ) { class Builder { @@ -40,12 +44,16 @@ data class RoomSummaryQueryParams( var displayName: QueryStringValue = QueryStringValue.IsNotEmpty var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition var memberships: List = Membership.all() + var roomCategoryFilter: RoomCategoryFilter? = RoomCategoryFilter.ALL + var roomTagQueryFilter: RoomTagQueryFilter? = null fun build() = RoomSummaryQueryParams( roomId = roomId, displayName = displayName, canonicalAlias = canonicalAlias, - memberships = memberships + memberships = memberships, + roomCategoryFilter = roomCategoryFilter, + roomTagQueryFilter = roomTagQueryFilter ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeRoomListDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableFilterLivePageResult.kt similarity index 61% rename from vector/src/main/java/im/vector/app/features/home/HomeRoomListDataSource.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableFilterLivePageResult.kt index 6bcd6f01eb..38462d5ac6 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeRoomListDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableFilterLivePageResult.kt @@ -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"); * 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 + * 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, @@ -14,12 +14,14 @@ * 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 javax.inject.Inject -import javax.inject.Singleton -@Singleton -class HomeRoomListDataSource @Inject constructor() : BehaviorDataSource>() +interface UpdatableFilterLivePageResult { + val livePagedList: LiveData> + + fun updateQuery(queryParams: RoomSummaryQueryParams) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomAggregateNotificationCount.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomAggregateNotificationCount.kt new file mode 100644 index 0000000000..066178b1ec --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomAggregateNotificationCount.kt @@ -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 +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index c7fe7ab447..1daae906f2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -17,22 +17,27 @@ package org.matrix.android.sdk.internal.database import io.realm.DynamicRealm +import io.realm.FieldAttribute 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.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.PendingThreePidEntityFields 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.RoomMembersLoadStatusType 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 javax.inject.Inject class RealmSessionStoreMigration @Inject constructor() : RealmMigration { 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) { @@ -46,6 +51,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { if (oldVersion <= 5) migrateTo6(realm) if (oldVersion <= 6) migrateTo7(realm) if (oldVersion <= 7) migrateTo8(realm) + if (oldVersion <= 8) migrateTo9(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -149,4 +155,43 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { ?.removeField("sourceLocalEchoEvents") ?.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) + } + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt index 2e54a4cd52..6dc70b60fc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt @@ -26,7 +26,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa private val typingUsersTracker: DefaultTypingUsersTracker) { fun map(roomSummaryEntity: RoomSummaryEntity): RoomSummary { - val tags = roomSummaryEntity.tags.map { + val tags = roomSummaryEntity.tags().map { RoomTag(it.tagName, it.tagOrder) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt index 37696c9082..c87ac15a78 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt @@ -16,61 +16,217 @@ 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.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.VersioningState -import io.realm.RealmList -import io.realm.RealmObject -import io.realm.annotations.PrimaryKey +import org.matrix.android.sdk.api.session.room.model.tag.RoomTag internal open class RoomSummaryEntity( - @PrimaryKey var roomId: String = "", - var displayName: String? = "", - var avatarUrl: String? = "", - var name: String? = "", - var topic: String? = "", - var latestPreviewableEvent: TimelineEventEntity? = null, - var heroes: RealmList = RealmList(), - var joinedMembersCount: Int? = 0, - var invitedMembersCount: Int? = 0, - var isDirect: Boolean = false, - var directUserId: String? = null, - var otherMemberIds: RealmList = RealmList(), - var notificationCount: Int = 0, - var highlightCount: Int = 0, - var readMarkerId: String? = null, - var hasUnreadMessages: Boolean = false, - var tags: RealmList = RealmList(), - var userDrafts: UserDraftsEntity? = null, - var breadcrumbsIndex: Int = RoomSummary.NOT_IN_BREADCRUMBS, - var canonicalAlias: String? = null, - var aliases: RealmList = 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 + @PrimaryKey var roomId: String = "" ) : 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 = 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 = 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 = RealmList() + + fun tags(): List = tags + + fun updateTags(newTags: List>) { + val toDelete = mutableListOf() + 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 = RealmList() + + fun updateAliases(newAliases: List) { + // 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 + var membership: Membership get() { return Membership.valueOf(membershipStr) } set(value) { - membershipStr = value.name + if (value.name != membershipStr) { + membershipStr = value.name + } } + @Index private var versioningStateStr: String = VersioningState.NONE.name var versioningState: VersioningState get() { return VersioningState.valueOf(versioningStateStr) } set(value) { - versioningStateStr = value.name + if (value.name != versioningStateStr) { + versioningStateStr = value.name + } } var roomEncryptionTrustLevel: RoomEncryptionTrustLevel? @@ -84,7 +240,9 @@ internal open class RoomSummaryEntity( } } set(value) { - roomEncryptionTrustLevelStr = value?.name + if (value?.name != roomEncryptionTrustLevelStr) { + roomEncryptionTrustLevelStr = value?.name + } } companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt index 383dd876d3..bd63ba480e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -18,17 +18,20 @@ package org.matrix.android.sdk.internal.session.room import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations +import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.RoomService 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.RoomMemberSummary 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.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.Optional import org.matrix.android.sdk.api.util.toOptional @@ -96,6 +99,20 @@ internal class DefaultRoomService @Inject constructor( return roomSummaryDataSource.getRoomSummariesLive(queryParams) } + override fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config) + : LiveData> { + 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 { return roomSummaryDataSource.getBreadcrumbs(queryParams) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt index fb3182d28b..bafe2b90ae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt @@ -17,18 +17,19 @@ package org.matrix.android.sdk.internal.session.room.create import com.zhuinden.monarchy.Monarchy +import io.realm.Realm import io.realm.RealmConfiguration import kotlinx.coroutines.TimeoutCancellationException import org.matrix.android.sdk.api.failure.Failure 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.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.CreateRoomPreset 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.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase 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) try { awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> - realm.where(RoomEntity::class.java) - .equalTo(RoomEntityFields.ROOM_ID, roomId) + realm.where(RoomSummaryEntity::class.java) + .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId) + .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) } } catch (exception: TimeoutCancellationException) { throw CreateRoomFailure.CreatedWithTimeout } + + Realm.getInstance(realmConfiguration).executeTransactionAsync { + RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = System.currentTimeMillis() + } + if (otherUserId != null) { handleDirectChatCreation(roomId, otherUserId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt index 9fd4d95d9e..33776e4f6e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt @@ -16,20 +16,23 @@ 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.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.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.RoomSummaryEntityFields +import org.matrix.android.sdk.internal.database.query.where 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.session.room.RoomAPI 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.task.Task -import io.realm.RealmConfiguration -import kotlinx.coroutines.TimeoutCancellationException -import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -68,12 +71,18 @@ internal class DefaultJoinRoomTask @Inject constructor( val roomId = joinRoomResponse.roomId try { awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> - realm.where(RoomEntity::class.java) - .equalTo(RoomEntityFields.ROOM_ID, roomId) + realm.where(RoomSummaryEntity::class.java) + .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId) + .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) } } catch (exception: TimeoutCancellationException) { throw JoinRoomFailure.JoinedWithTimeout } + + Realm.getInstance(realmConfiguration).executeTransactionAsync { + RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = System.currentTimeMillis() + } + setReadMarkers(roomId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt index 107055b8c3..dd3fbe04b2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt @@ -18,10 +18,18 @@ package org.matrix.android.sdk.internal.session.room.summary import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList 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.UpdatableFilterLivePageResult 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.summary.RoomAggregateNotificationCount import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional 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.query.process import org.matrix.android.sdk.internal.util.fetchCopyMap -import io.realm.Realm -import io.realm.RealmQuery import javax.inject.Inject 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) } + fun getSortedPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, + pagedListConfig: PagedList.Config): LiveData> { + 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> = 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 { val query = RoomSummaryEntity.where(realm) 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.MEMBERSHIP_STR, queryParams.memberships) 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 } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index cd1bb69612..f254c44fda 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -98,6 +98,11 @@ internal class RoomSummaryUpdater @Inject constructor( val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) + val lastActivityFromEvent = latestPreviewableEvent?.root?.originServerTs + if (lastActivityFromEvent != null) { + roomSummaryEntity.lastActivityTime = lastActivityFromEvent + } + roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 // avoid this call if we are sure there are unread events || !isEventRead(realm.configuration, userId, roomId, latestPreviewableEvent?.eventId) @@ -112,9 +117,7 @@ internal class RoomSummaryUpdater @Inject constructor( val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel()?.aliases .orEmpty() - roomSummaryEntity.aliases.clear() - roomSummaryEntity.aliases.addAll(roomAliases) - roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|") + roomSummaryEntity.updateAliases(roomAliases) roomSummaryEntity.isEncrypted = encryptionEvent != null roomSummaryEntity.encryptionEventTs = encryptionEvent?.originServerTs diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt index f9ae41bc94..add5d841d1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt @@ -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.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomTagEntity -import org.matrix.android.sdk.internal.database.query.where import io.realm.Realm +import org.matrix.android.sdk.internal.database.query.getOrCreate import javax.inject.Inject internal class RoomTagHandler @Inject constructor() { @@ -31,12 +31,8 @@ internal class RoomTagHandler @Inject constructor() { } val tags = content.tags.entries.map { (tagName, params) -> RoomTagEntity(tagName, params["order"] as? Double) + Pair(tagName, params["order"] as? Double) } - val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: RoomSummaryEntity(roomId) - - roomSummaryEntity.tags.clear() - roomSummaryEntity.tags.addAll(tags) - realm.insertOrUpdate(roomSummaryEntity) + RoomSummaryEntity.getOrCreate(realm, roomId).updateTags(tags) } } diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 7b7c44b9fd..5a53ececec 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -161,7 +161,7 @@ Formatter\.formatShortFileSize===1 # 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 -enum class===93 +enum class===94 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/vector/src/main/java/im/vector/app/AppStateHandler.kt b/vector/src/main/java/im/vector/app/AppStateHandler.kt index 1e92f7bc67..edec704f18 100644 --- a/vector/src/main/java/im/vector/app/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/app/AppStateHandler.kt @@ -19,78 +19,26 @@ package im.vector.app import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver 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.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.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 */ +// TODO Keep this class for now, will maybe be used fro Space @Singleton -class AppStateHandler @Inject constructor( - private val sessionDataSource: ActiveSessionDataSource, - private val homeRoomListDataSource: HomeRoomListDataSource, - private val selectedGroupDataSource: SelectedGroupDataSource, - private val chronologicalRoomComparator: ChronologicalRoomComparator) : LifecycleObserver { +class AppStateHandler @Inject constructor() : LifecycleObserver { private val compositeDisposable = CompositeDisposable() @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun entersForeground() { - observeRoomsAndGroup() } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun entersBackground() { compositeDisposable.clear() } - - private fun observeRoomsAndGroup() { - Observable - .combineLatest, Option, List>( - 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) - } } diff --git a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt index 23d6b618fe..cae7a2ece6 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt @@ -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.grouplist.SelectedGroupDataSource 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.timeline.helper.MatrixItemColorProvider import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder @@ -113,8 +112,6 @@ interface VectorComponent { fun errorFormatter(): ErrorFormatter - fun homeRoomListObservableStore(): HomeRoomListDataSource - fun selectedGroupStore(): SelectedGroupDataSource fun roomDetailPendingActionStore(): RoomDetailPendingActionStore diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt index f515060db6..258517aa39 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt @@ -127,6 +127,12 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScre Timber.i("onResume Fragment ${javaClass.simpleName}") } + @CallSuper + override fun onPause() { + super.onPause() + Timber.i("onPause Fragment ${javaClass.simpleName}") + } + @CallSuper override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -149,7 +155,9 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScre super.onDestroyView() } + @CallSuper override fun onDestroy() { + Timber.i("onDestroy Fragment ${javaClass.simpleName}") uiDisposables.dispose() super.onDestroy() } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailAction.kt index 447820ed7b..c64f9d453d 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailAction.kt @@ -20,4 +20,5 @@ import im.vector.app.core.platform.VectorViewModelAction sealed class HomeDetailAction : VectorViewModelAction { data class SwitchDisplayMode(val displayMode: RoomListDisplayMode) : HomeDetailAction() + object MarkAllRoomsRead : HomeDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index 4c7b7aa991..5def43b60b 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -18,6 +18,8 @@ package im.vector.app.features.home import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem import android.view.View import android.view.ViewGroup 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.VectorBaseFragment 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.KnownCallsViewHolder import im.vector.app.databinding.FragmentHomeDetailBinding import im.vector.app.features.call.SharedKnownCallsViewModel 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.ServerBackupStatusViewModel import im.vector.app.features.workers.signout.ServerBackupStatusViewState - import org.matrix.android.sdk.api.session.group.model.GroupSummary import org.matrix.android.sdk.api.util.toMatrixItem 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 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 { 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_notification).render(it.notificationCountCatchup, it.notificationHighlightCatchup) views.syncStateView.render(it.syncState) + + hasUnreadRooms = it.hasUnreadMessages } private fun BadgeDrawable.render(count: Int, highlight: Boolean) { diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt index c261081055..c87b19f0e6 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt @@ -16,22 +16,30 @@ package im.vector.app.features.home +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.di.HasScreenInjector import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.grouplist.SelectedGroupDataSource 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.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 timber.log.Timber +import java.util.concurrent.TimeUnit /** * 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 uiStateRepository: UiStateRepository, private val selectedGroupStore: SelectedGroupDataSource, - private val homeRoomListStore: HomeRoomListDataSource, private val stringProvider: StringProvider) : VectorViewModel(initialState) { @@ -75,6 +82,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho override fun handle(action: HomeDetailAction) { when (action) { is HomeDetailAction.SwitchDisplayMode -> handleSwitchDisplayMode(action) + HomeDetailAction.MarkAllRoomsRead -> handleMarkAllRoomsRead() } } @@ -90,6 +98,26 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho // 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 { + session.markAllAsRead(roomIds, it) + } + } catch (failure: Throwable) { + Timber.d(failure, "Failed to mark all as read") + } + } + } + private fun observeSyncState() { session.rx() .liveSyncState() @@ -113,43 +141,51 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho } private fun observeRoomSummaries() { - homeRoomListStore - .observe() - .observeOn(Schedulers.computation()) - .map { it.asSequence() } - .subscribe { summaries -> - val invitesDm = summaries - .filter { it.membership == Membership.INVITE && it.isDirect } - .count() + session.getPagedRoomSummariesLive( + roomSummaryQueryParams { + memberships = Membership.activeMemberships() + } + ) + .asObservable() + .throttleFirst(300, TimeUnit.MILLISECONDS) + .subscribe { + val dmInvites = session.getRoomSummaries( + roomSummaryQueryParams { + memberships = listOf(Membership.INVITE) + roomCategoryFilter = RoomCategoryFilter.ONLY_DM + } + ).size - val invitesRoom = summaries - .filter { it.membership == Membership.INVITE && it.isDirect.not() } - .count() + val roomsInvite = session.getRoomSummaries( + roomSummaryQueryParams { + memberships = listOf(Membership.INVITE) + roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + } + ).size - val peopleNotifications = summaries - .filter { it.isDirect } - .map { it.notificationCount } - .sum() - val peopleHasHighlight = summaries - .filter { it.isDirect } - .any { it.highlightCount > 0 } + val dmRooms = session.getNotificationCountForRooms( + roomSummaryQueryParams { + memberships = listOf(Membership.JOIN) + roomCategoryFilter = RoomCategoryFilter.ONLY_DM + } + ) - val roomsNotifications = summaries - .filter { !it.isDirect } - .map { it.notificationCount } - .sum() - val roomsHasHighlight = summaries - .filter { !it.isDirect } - .any { it.highlightCount > 0 } + val otherRooms = session.getNotificationCountForRooms( + roomSummaryQueryParams { + memberships = listOf(Membership.JOIN) + roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS + } + ) setState { copy( - notificationCountCatchup = peopleNotifications + roomsNotifications + invitesDm + invitesRoom, - notificationHighlightCatchup = peopleHasHighlight || roomsHasHighlight, - notificationCountPeople = peopleNotifications + invitesDm, - notificationHighlightPeople = peopleHasHighlight || invitesDm > 0, - notificationCountRooms = roomsNotifications + invitesRoom, - notificationHighlightRooms = roomsHasHighlight || invitesRoom > 0 + notificationCountCatchup = dmRooms.totalCount + otherRooms.totalCount + roomsInvite + dmInvites, + notificationHighlightCatchup = dmRooms.isHighlight || otherRooms.isHighlight, + notificationCountPeople = dmRooms.totalCount + dmInvites, + notificationHighlightPeople = dmRooms.isHighlight || dmInvites > 0, + notificationCountRooms = otherRooms.totalCount + roomsInvite, + notificationHighlightRooms = otherRooms.isHighlight || roomsInvite > 0, + hasUnreadMessages = dmRooms.totalCount + otherRooms.totalCount > 0 ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt index f5e4bc9fa3..533c9166f9 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt @@ -34,5 +34,6 @@ data class HomeDetailViewState( val notificationHighlightPeople: Boolean = false, val notificationCountRooms: Int = 0, val notificationHighlightRooms: Boolean = false, + val hasUnreadMessages: Boolean = false, val syncState: SyncState = SyncState.Idle ) : MvRxState diff --git a/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt b/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt index 3684a8b3f8..4a2d001e1d 100644 --- a/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt +++ b/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt @@ -21,36 +21,44 @@ import android.content.pm.ShortcutManager import android.os.Build import androidx.core.content.getSystemService 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.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 class ShortcutsHandler @Inject constructor( private val context: Context, - private val homeRoomListStore: HomeRoomListDataSource, - private val shortcutCreator: ShortcutCreator + private val shortcutCreator: ShortcutCreator, + private val activeSessionHolder: ActiveSessionHolder ) { fun observeRoomsAndBuildShortcuts(): Disposable { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { // No op - return Observable.empty().subscribe() + return Disposables.empty() } - return homeRoomListStore - .observe() - .distinctUntilChanged() - .observeOn(Schedulers.computation()) - .subscribe { rooms -> + return activeSessionHolder.getSafeActiveSession() + ?.getPagedRoomSummariesLive( + roomSummaryQueryParams { + memberships = listOf(Membership.JOIN) + roomTagQueryFilter = RoomTagQueryFilter(isFavorite = true, null, null) + } + ) + ?.asObservable() + ?.subscribe { rooms -> val shortcuts = rooms - .filter { room -> room.isFavorite } .take(n = 4) // Android only allows us to create 4 shortcuts .map { shortcutCreator.create(it) } ShortcutManagerCompat.removeAllDynamicShortcuts(context) ShortcutManagerCompat.addDynamicShortcuts(context, shortcuts) } + ?: Disposables.empty() } fun clearShortcuts() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt index 4a6c1c16fc..883efb2e60 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt @@ -22,12 +22,11 @@ import org.matrix.android.sdk.api.session.room.notification.RoomNotificationStat sealed class RoomListAction : VectorViewModelAction { 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 RejectInvitation(val roomSummary: RoomSummary) : RoomListAction() data class FilterWith(val filter: String) : RoomListAction() data class ChangeRoomNotificationState(val roomId: String, val notificationState: RoomNotificationState) : RoomListAction() data class ToggleTag(val roomId: String, val tag: String) : RoomListAction() data class LeaveRoom(val roomId: String) : RoomListAction() - object MarkAllRoomsRead : RoomListAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFooterController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFooterController.kt new file mode 100644 index 0000000000..d4e062d1e4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFooterController.kt @@ -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() { + + 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)) + } + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index 30cb360a9d..aaa5bbcde5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -20,19 +20,15 @@ import android.content.DialogInterface import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.core.view.isVisible +import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView 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.fragmentViewModel 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.StateView import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.databinding.FragmentRoomListBinding import im.vector.app.features.home.RoomListDisplayMode 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.notifications.NotificationDrawerManager import kotlinx.parcelize.Parcelize -import org.matrix.android.sdk.api.failure.Failure -import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.extensions.orTrue 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.notification.RoomNotificationState @@ -66,12 +62,13 @@ data class RoomListParams( ) : Parcelable class RoomListFragment @Inject constructor( - private val roomController: RoomSummaryController, + private val pagedControllerFactory: RoomSummaryPagedControllerFactory, val roomListViewModelFactory: RoomListViewModel.Factory, private val notificationDrawerManager: NotificationDrawerManager, - private val sharedViewPool: RecyclerView.RecycledViewPool + private val footerController: RoomListFooterController, + private val userPreferencesProvider: UserPreferencesProvider ) : VectorBaseFragment(), - RoomSummaryController.Listener, + RoomListListener, OnBackPressed, NotifsFabMenuView.Listener { @@ -85,28 +82,25 @@ class RoomListFragment @Inject constructor( 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 { - when (item.itemId) { - 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) - } + private val adapterInfosList = mutableListOf() + private var concatAdapter : ConcatAdapter? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + views.stateView.contentView = views.roomListView + views.stateView.state = StateView.State.Loading setupCreateRoomButton() setupRecyclerView() sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) @@ -125,6 +119,40 @@ class RoomListFragment @Inject constructor( .observe() .subscribe { handleQuickActions(it) } .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) { @@ -132,12 +160,15 @@ class RoomListFragment @Inject constructor( } override fun onDestroyView() { - roomController.removeModelBuildListener(modelBuildListener) + adapterInfosList.onEach { it.contentAdapter.removeModelBuildListener(modelBuildListener) } + adapterInfosList.clear() modelBuildListener = null views.roomListView.cleanup() - roomController.listener = null + footerController.listener = null + // TODO Cleanup listener on the ConcatAdapter's adapters? stateRestorer.clear() views.createChatFabMenu.listener = null + concatAdapter = null super.onDestroyView() } @@ -204,13 +235,58 @@ class RoomListFragment @Inject constructor( stateRestorer = LayoutManagerStateRestorer(layoutManager).register() views.roomListView.layoutManager = layoutManager views.roomListView.itemAnimator = RoomListAnimator() - views.roomListView.setRecycledViewPool(sharedViewPool) layoutManager.recycleChildrenOnDetach = true - roomController.listener = this + modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) } - roomController.addModelBuildListener(modelBuildListener) - views.roomListView.adapter = roomController.adapter - views.stateView.contentView = views.roomListView + + val concatAdapter = ConcatAdapter() + + 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 { @@ -278,89 +354,41 @@ class RoomListFragment @Inject constructor( } override fun invalidate() = withState(roomListViewModel) { state -> - when (state.asyncFilteredRooms) { - 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 - } + footerController.setData(state) } - private fun renderSuccess(state: RoomListViewState) { - val allRooms = state.asyncRooms() - val filteredRooms = state.asyncFilteredRooms() - if (filteredRooms.isNullOrEmpty()) { - renderEmptyState(allRooms) - } else { - views.stateView.state = StateView.State.Content - } - } - - private fun renderEmptyState(allRooms: List?) { - 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 { + private fun checkEmptyState() { + val hasNoRoom = adapterInfosList.all { it.headerHeaderAdapter.roomsSectionData.isHidden } + if (hasNoRoom) { + val emptyState = when (roomListParams.displayMode) { + RoomListDisplayMode.NOTIFICATIONS -> { StateView.State.Empty( title = getString(R.string.room_list_catchup_empty_title), image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_noun_party_popper), 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 -> - 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 + views.stateView.state = emptyState + } else { + views.stateView.state = 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 { @@ -377,7 +405,11 @@ class RoomListFragment @Inject constructor( } override fun onRoomLongClicked(room: RoomSummary): Boolean { - roomController.onRoomLongClicked() + userPreferencesProvider.neverShowLongClickOnRoomHelpAgain() + withState(roomListViewModel) { + // refresh footer + footerController.setData(it) + } RoomListQuickActionsBottomSheet .newInstance(room.roomId, RoomListActionsArgs.Mode.FULL) .show(childFragmentManager, "ROOM_LIST_QUICK_ACTIONS") @@ -394,10 +426,6 @@ class RoomListFragment @Inject constructor( roomListViewModel.handle(RoomListAction.RejectInvitation(room)) } - override fun onToggleRoomCategory(roomCategory: RoomCategory) { - roomListViewModel.handle(RoomListAction.ToggleCategory(roomCategory)) - } - override fun createRoom(initialName: String) { navigator.openCreateRoom(requireActivity(), initialName) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListListener.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListListener.kt new file mode 100644 index 0000000000..e9833d1560 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListListener.kt @@ -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) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt index 3a5e797f98..423a950591 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt @@ -16,37 +16,61 @@ package im.vector.app.features.home.room.list +import androidx.annotation.StringRes import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext +import im.vector.app.R import im.vector.app.core.extensions.exhaustive 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 kotlinx.coroutines.Dispatchers 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.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.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.RoomSummary 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.rx.asObservable import org.matrix.android.sdk.rx.rx import timber.log.Timber -import java.lang.Exception import javax.inject.Inject -class RoomListViewModel @Inject constructor(initialState: RoomListViewState, - private val session: Session, - private val roomSummariesSource: DataSource>) - : VectorViewModel(initialState) { +class RoomListViewModel @Inject constructor( + initialState: RoomListViewState, + private val session: Session, + private val stringProvider: StringProvider +) : VectorViewModel(initialState) { interface Factory { 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 { @JvmStatic @@ -56,28 +80,136 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, } } - private val displayMode = initialState.displayMode - private val roomListDisplayModeFilter = RoomListDisplayModeFilter(displayMode) + val sections: List by lazy { + val sections = mutableListOf() + if (initialState.displayMode == RoomListDisplayMode.PEOPLE) { + addSection(sections, R.string.invitations_header, true) { + it.memberships = listOf(Membership.INVITE) + it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM + } - init { - observeRoomSummaries() - observeMembershipChanges() + addSection(sections, R.string.bottom_action_favourites) { + it.memberships = listOf(Membership.JOIN) + 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) { when (action) { is RoomListAction.SelectRoom -> handleSelectRoom(action) - is RoomListAction.ToggleCategory -> handleToggleCategory(action) is RoomListAction.AcceptInvitation -> handleAcceptInvitation(action) is RoomListAction.RejectInvitation -> handleRejectInvitation(action) is RoomListAction.FilterWith -> handleFilter(action) - is RoomListAction.MarkAllRoomsRead -> handleMarkAllRoomsRead() is RoomListAction.LeaveRoom -> handleLeaveRoom(action) is RoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) is RoomListAction.ToggleTag -> handleToggleTag(action) + is RoomListAction.ToggleSection -> handleToggleSection(action.section) }.exhaustive } + private fun addSection(sections: MutableList, + @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 { return session.getRoom(roomId)?.isPublic().orFalse() } @@ -88,8 +220,14 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, _viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary)) } - private fun handleToggleCategory(action: RoomListAction.ToggleCategory) = setState { - this.toggle(action.category) + private fun handleToggleSection(roomSection: RoomsSection) { + 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) { @@ -98,23 +236,12 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, roomFilter = action.filter ) } - } - - private fun observeRoomSummaries() { - roomSummariesSource - .observe() - .observeOn(Schedulers.computation()) - .execute { asyncRooms -> - copy(asyncRooms = asyncRooms) - } - - roomSummariesSource - .observe() - .observeOn(Schedulers.computation()) - .map { buildRoomSummaries(it) } - .execute { async -> - copy(asyncFilteredRooms = async) + updatableQuery?.updateQuery( + roomSummaryQueryParams { + memberships = Membership.activeMemberships() + displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.INSENSITIVE) } + ) } private fun handleAcceptInvitation(action: RoomListAction.AcceptInvitation) = withState { state -> @@ -126,6 +253,19 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, 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 viewModelScope.launch { 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) { val room = session.getRoom(action.roomId) if (room != null) { @@ -226,46 +357,4 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, _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): RoomSummaries { - // Set up init size on directChats and groupRooms as they are the biggest ones - val invites = ArrayList() - val favourites = ArrayList() - val directChats = ArrayList(rooms.size) - val groupRooms = ArrayList(rooms.size) - val lowPriorities = ArrayList() - val serverNotices = ArrayList() - - 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) - } - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModelFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModelFactory.kt index 44ca8cefda..d36bc45ab6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModelFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModelFactory.kt @@ -16,20 +16,20 @@ 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 javax.inject.Inject import javax.inject.Provider class RoomListViewModelFactory @Inject constructor(private val session: Provider, - private val homeRoomListDataSource: Provider) + private val stringProvider: StringProvider) : RoomListViewModel.Factory { override fun create(initialState: RoomListViewState): RoomListViewModel { return RoomListViewModel( initialState, session.get(), - homeRoomListDataSource.get() + stringProvider ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt index 095262d74b..104a3710f7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt @@ -16,73 +16,15 @@ 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.Uninitialized -import im.vector.app.R 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.model.Membership -import org.matrix.android.sdk.api.session.room.model.RoomSummary data class RoomListViewState( val displayMode: RoomListDisplayMode, - val asyncRooms: Async> = Uninitialized, val roomFilter: String = "", - val asyncFilteredRooms: Async = Uninitialized, - val roomMembershipChanges: Map = 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 + val roomMembershipChanges: Map = emptyMap() ) : MvRxState { 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> - -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() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryController.kt deleted file mode 100644 index d7cace9edb..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryController.kt +++ /dev/null @@ -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, - @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, - roomChangedMembershipStates: Map, - selectedRoomIds: Set) { - 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) - } -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index 7d7ed1637f..fa6c970d8a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -40,7 +40,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor fun create(roomSummary: RoomSummary, roomChangeMembershipStates: Map, selectedRoomIds: Set, - listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> { + listener: RoomListListener?): VectorEpoxyModel<*> { return when (roomSummary.membership) { Membership.INVITE -> { val changeMembershipState = roomChangeMembershipStates[roomSummary.roomId] ?: ChangeMembershipState.Unknown @@ -52,7 +52,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor private fun createInvitationItem(roomSummary: RoomSummary, changeMembershipState: ChangeMembershipState, - listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> { + listener: RoomListListener?): VectorEpoxyModel<*> { val secondLine = if (roomSummary.isDirect) { roomSummary.inviterId } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt new file mode 100644 index 0000000000..20386d739a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryPagedController.kt @@ -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( + // Important it must match the PageList builder notify Looper + modelBuildingHandler = createUIHandler() +) { + + var listener: RoomListListener? = null + + var roomChangeMembershipStates: Map? = 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) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomsSection.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomsSection.kt new file mode 100644 index 0000000000..71b7169814 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomsSection.kt @@ -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>, + val isExpanded: MutableLiveData = MutableLiveData(true), + val notificationCount: MutableLiveData = MutableLiveData(RoomAggregateNotificationCount(0, 0)), + val notifyOfLocalEcho: Boolean = false +) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt b/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt new file mode 100644 index 0000000000..f9c5766821 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/SectionHeaderAdapter.kt @@ -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() { + + 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) + } + } + } +}