Room summary paged initial commit

This commit is contained in:
Valere 2021-03-31 10:26:37 +02:00 committed by Benoit Marty
parent c0913711d6
commit cf868f885f
26 changed files with 1007 additions and 445 deletions

View File

@ -33,3 +33,4 @@ sealed class QueryStringValue {
INSENSITIVE
}
}

View File

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

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.api.session.room
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
@ -26,7 +27,9 @@ 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.util.Cancelable
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.room.UpdatableFilterLivePageResult
import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
/**
* This interface defines methods to get rooms. It's implemented at the session level.
@ -178,4 +181,9 @@ 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<PeekResult>)
fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData<PagedList<RoomSummary>>
fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount
fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams): UpdatableFilterLivePageResult
}

View File

@ -17,12 +17,19 @@
package org.matrix.android.sdk.api.session.room
import org.matrix.android.sdk.api.query.QueryStringValue
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 {
return RoomSummaryQueryParams.Builder().apply(init).build()
}
enum class RoomCategoryFilter {
ONLY_DM,
ONLY_ROOMS,
ALL
}
/**
* This class can be used to filter room summaries to use with:
* [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService]
@ -31,7 +38,9 @@ data class RoomSummaryQueryParams(
val roomId: QueryStringValue,
val displayName: QueryStringValue,
val canonicalAlias: QueryStringValue,
val memberships: List<Membership>
val memberships: List<Membership>,
val roomCategoryFilter: RoomCategoryFilter?,
val roomTagQueryFilter: RoomTagQueryFilter?
) {
class Builder {
@ -40,12 +49,16 @@ data class RoomSummaryQueryParams(
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
var memberships: List<Membership> = 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
)
}
}

View File

@ -0,0 +1,26 @@
/*
* 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 org.matrix.android.sdk.api.session.room.summary
data class RoomAggregateNotificationCount(
val notificationCount: Int,
val highlightCount: Int
) {
fun totalCount() = notificationCount + highlightCount
fun isHighlight() = highlightCount > 0
}

View File

@ -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)
}
}
}
}

View File

@ -72,6 +72,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
.allowWritesOnUiThread(true)
.modules(SessionRealmModule())
.schemaVersion(RealmSessionStoreMigration.SESSION_STORE_SCHEMA_VERSION)
// .deleteRealmIfMigrationNeeded()
.migration(migration)
.build()

View File

@ -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)
}

View File

@ -16,61 +16,221 @@
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
import timber.log.Timber
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<String> = RealmList(),
var joinedMembersCount: Int? = 0,
var invitedMembersCount: Int? = 0,
var isDirect: Boolean = false,
var directUserId: String? = null,
var otherMemberIds: RealmList<String> = RealmList(),
var notificationCount: Int = 0,
var highlightCount: Int = 0,
var readMarkerId: String? = null,
var hasUnreadMessages: Boolean = false,
var tags: RealmList<RoomTagEntity> = RealmList(),
var userDrafts: UserDraftsEntity? = null,
var breadcrumbsIndex: Int = RoomSummary.NOT_IN_BREADCRUMBS,
var canonicalAlias: String? = null,
var aliases: RealmList<String> = RealmList(),
// this is required for querying
var flatAliases: String = "",
var isEncrypted: Boolean = false,
var encryptionEventTs: Long? = 0,
var roomEncryptionTrustLevelStr: String? = null,
var inviterId: String? = null,
var hasFailedSending: Boolean = false
@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<String> = RealmList()
var joinedMembersCount: Int? = 0
set(value) {
if (value != field) field = value
}
var invitedMembersCount: Int? = 0
set(value) {
if (value != field) field = value
}
@Index
var isDirect: Boolean = false
set(value) {
if (value != field) field = value
}
var directUserId: String? = null
set(value) {
if (value != field) field = value
}
var otherMemberIds: RealmList<String> = RealmList()
var notificationCount: Int = 0
set(value) {
if (value != field) field = value
}
var highlightCount: Int = 0
set(value) {
if (value != field) field = value
}
var readMarkerId: String? = null
set(value) {
if (value != field) field = value
}
var hasUnreadMessages: Boolean = false
set(value) {
if (value != field) field = value
}
private var tags: RealmList<RoomTagEntity> = RealmList()
fun tags(): RealmList<RoomTagEntity> = tags
fun updateTags(newTags: List<Pair<String, Double?>>) {
val toDelete = mutableListOf<RoomTagEntity>()
tags.forEach { existingTag ->
val updatedTag = newTags.firstOrNull { it.first == existingTag.tagName }
if (updatedTag == null) {
toDelete.add(existingTag)
} else {
existingTag.tagOrder = updatedTag.second
}
}
toDelete.onEach { it.deleteFromRealm() }
newTags.forEach { newTag ->
if (tags.indexOfFirst { it.tagName == newTag.first } == -1) {
// we must add it
tags.add(
RoomTagEntity(newTag.first, newTag.second)
)
}
}
isFavourite = newTags.indexOfFirst { it.first == RoomTag.ROOM_TAG_FAVOURITE } != -1
isLowPriority = newTags.indexOfFirst { it.first == RoomTag.ROOM_TAG_LOW_PRIORITY } != -1
isServerNotice = newTags.indexOfFirst { it.first == RoomTag.ROOM_TAG_SERVER_NOTICE } != -1
}
@Index
var isFavourite: Boolean = false
set(value) {
if (value != field) field = value
}
@Index
var isLowPriority: Boolean = false
set(value) {
if (value != field) field = value
}
@Index
var isServerNotice: Boolean = false
set(value) {
if (value != field) field = value
}
var userDrafts: UserDraftsEntity? = null
set(value) {
if (value != field) field = value
}
var breadcrumbsIndex: Int = RoomSummary.NOT_IN_BREADCRUMBS
set(value) {
if (value != field) field = value
}
var canonicalAlias: String? = null
set(value) {
if (value != field) field = value
}
var aliases: RealmList<String> = RealmList()
fun updateAliases(newAliases: List<String>) {
// only update underlying field if there is a diff
if (newAliases.toSet() != aliases.toSet()) {
Timber.w("VAL: aliases updated")
aliases.clear()
aliases.addAll(newAliases)
}
}
// this is required for querying
var flatAliases: String = ""
set(value) {
if (value != field) field = value
}
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 +244,9 @@ internal open class RoomSummaryEntity(
}
}
set(value) {
roomEncryptionTrustLevelStr = value?.name
if (value?.name != roomEncryptionTrustLevelStr) {
roomEncryptionTrustLevelStr = value?.name
}
}
companion object

View File

@ -18,6 +18,7 @@ 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
@ -45,6 +46,7 @@ import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomT
import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask
import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask
import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask
import org.matrix.android.sdk.internal.task.TaskExecutor
@ -96,6 +98,18 @@ internal class DefaultRoomService @Inject constructor(
return roomSummaryDataSource.getRoomSummariesLive(queryParams)
}
override fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams) : LiveData<PagedList<RoomSummary>> {
return roomSummaryDataSource.getSortedPagedRoomSummariesLive(queryParams)
}
override fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams) : UpdatableFilterLivePageResult {
return roomSummaryDataSource.getFilteredPagedRoomSummariesLive(queryParams)
}
override fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
return roomSummaryDataSource.getNotificationCountForRooms(queryParams)
}
override fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List<RoomSummary> {
return roomSummaryDataSource.getBreadcrumbs(queryParams)
}
@ -178,3 +192,8 @@ internal class DefaultRoomService @Inject constructor(
.executeBy(taskExecutor)
}
}
interface UpdatableFilterLivePageResult {
val livePagedList: LiveData<PagedList<RoomSummary>>
fun updateQuery(queryParams: RoomSummaryQueryParams)
}

View File

@ -18,10 +18,17 @@ 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.session.room.RoomCategoryFilter
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
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
@ -31,9 +38,8 @@ import org.matrix.android.sdk.internal.database.query.findByAlias
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.session.room.UpdatableFilterLivePageResult
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,71 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
.sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX)
}
fun getSortedPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData<PagedList<RoomSummary>> {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
roomSummariesQuery(realm, queryParams)
.sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING)
}
val dataSourceFactory = realmDataSourceFactory.map {
roomSummaryMapper.map(it)
}
return monarchy.findAllPagedWithChanges(realmDataSourceFactory,
LivePagedListBuilder(dataSourceFactory,
PagedList.Config.Builder()
.setPageSize(10)
.setInitialLoadSizeHint(20)
.setEnablePlaceholders(false)
.setPrefetchDistance(10)
.build())
)
}
fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams): 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,
PagedList.Config.Builder()
.setPageSize(10)
.setInitialLoadSizeHint(20)
.setEnablePlaceholders(false)
.setPrefetchDistance(10)
.build())
)
return object : UpdatableFilterLivePageResult {
override val livePagedList: LiveData<PagedList<RoomSummary>>
get() = mapped
override fun updateQuery(queryParams: RoomSummaryQueryParams) {
realmDataSourceFactory.updateQuery {
roomSummariesQuery(it, queryParams)
.sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING)
}
}
}
}
fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
var notificationCount: RoomAggregateNotificationCount? = null
monarchy.doWithRealm { realm ->
val roomSummariesQuery = roomSummariesQuery(realm, queryParams)
val notifCount = roomSummariesQuery.sum(RoomSummaryEntityFields.NOTIFICATION_COUNT).toInt()
val highlightCount = roomSummariesQuery.sum(RoomSummaryEntityFields.HIGHLIGHT_COUNT).toInt()
notificationCount = RoomAggregateNotificationCount(
notifCount,
highlightCount
)
}
return notificationCount!!
}
private fun roomSummariesQuery(realm: Realm, queryParams: RoomSummaryQueryParams): RealmQuery<RoomSummaryEntity> {
val query = RoomSummaryEntity.where(realm)
query.process(RoomSummaryEntityFields.ROOM_ID, queryParams.roomId)
@ -105,6 +176,27 @@ 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.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 { lp ->
query.equalTo(RoomSummaryEntityFields.IS_SERVER_NOTICE, lp)
}
}
return query
}
}

View File

@ -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,8 +117,9 @@ internal class RoomSummaryUpdater @Inject constructor(
val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases
.orEmpty()
roomSummaryEntity.aliases.clear()
roomSummaryEntity.aliases.addAll(roomAliases)
// roomSummaryEntity.aliases.clear()
// roomSummaryEntity.aliases.addAll(roomAliases)
roomSummaryEntity.updateAliases(roomAliases)
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
roomSummaryEntity.isEncrypted = encryptionEvent != null
roomSummaryEntity.encryptionEventTs = encryptionEvent?.originServerTs

View File

@ -19,8 +19,8 @@ package org.matrix.android.sdk.internal.session.sync
import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent
import org.matrix.android.sdk.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)
}
}

View File

@ -52,7 +52,7 @@ class AppStateHandler @Inject constructor(
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun entersForeground() {
observeRoomsAndGroup()
// observeRoomsAndGroup()
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)

View File

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

View File

@ -22,7 +22,7 @@ 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: RoomListViewModel.RoomsSection) : RoomListAction()
data class AcceptInvitation(val roomSummary: RoomSummary) : RoomListAction()
data class RejectInvitation(val roomSummary: RoomSummary) : RoomListAction()
data class FilterWith(val filter: String) : RoomListAction()

View File

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

View File

@ -27,12 +27,10 @@ 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 +42,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 +52,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 +64,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<FragmentRoomListBinding>(),
RoomSummaryController.Listener,
RoomListListener,
OnBackPressed,
NotifsFabMenuView.Listener {
@ -87,6 +86,20 @@ class RoomListFragment @Inject constructor(
private var hasUnreadRooms = false
data class SectionKey(
val name: String,
val isExpanded: Boolean
)
data class SectionAdapterInfo(
var section: SectionKey,
val headerHeaderAdapter: SectionHeaderAdapter,
val contentAdapter: RoomSummaryPagedController
)
private val adapterInfosList = mutableListOf<SectionAdapterInfo>()
private var concatAdapter: ConcatAdapter? = null
override fun getMenuRes() = R.menu.room_list
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -112,10 +125,10 @@ class RoomListFragment @Inject constructor(
sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
roomListViewModel.observeViewEvents {
when (it) {
is RoomListViewEvents.Loading -> showLoading(it.message)
is RoomListViewEvents.Failure -> showFailure(it.throwable)
is RoomListViewEvents.Loading -> showLoading(it.message)
is RoomListViewEvents.Failure -> showFailure(it.throwable)
is RoomListViewEvents.SelectRoom -> handleSelectRoom(it)
is RoomListViewEvents.Done -> Unit
is RoomListViewEvents.Done -> Unit
}.exhaustive
}
@ -127,15 +140,49 @@ class RoomListFragment @Inject constructor(
.disposeOnDestroyView()
}
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.section.copy(isExpanded = isRoomSectionExpanded)
)
}
}
override fun showFailure(throwable: Throwable) {
showErrorInSnackbar(throwable)
}
override fun onDestroyView() {
roomController.removeModelBuildListener(modelBuildListener)
adapterInfosList.onEach { it.contentAdapter.removeModelBuildListener(modelBuildListener) }
adapterInfosList.clear()
// roomController.removeModelBuildListener(modelBuildListener)
modelBuildListener = null
views.roomListView.cleanup()
roomController.listener = null
// controllers.onEach {
// it.listener = null
// // concatAdapter.removeAdapter(it.adapter)
// }
// controllers.clear()
// roomController.listener = null
// favRoomController.listener = null
stateRestorer.clear()
views.createChatFabMenu.listener = null
super.onDestroyView()
@ -148,8 +195,8 @@ class RoomListFragment @Inject constructor(
private fun setupCreateRoomButton() {
when (roomListParams.displayMode) {
RoomListDisplayMode.NOTIFICATIONS -> views.createChatFabMenu.isVisible = true
RoomListDisplayMode.PEOPLE -> views.createChatRoomButton.isVisible = true
RoomListDisplayMode.ROOMS -> views.createGroupRoomButton.isVisible = true
RoomListDisplayMode.PEOPLE -> views.createChatRoomButton.isVisible = true
RoomListDisplayMode.ROOMS -> views.createGroupRoomButton.isVisible = true
else -> Unit // No button in this mode
}
@ -204,21 +251,68 @@ class RoomListFragment @Inject constructor(
stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
views.roomListView.layoutManager = layoutManager
views.roomListView.itemAnimator = RoomListAnimator()
views.roomListView.setRecycledViewPool(sharedViewPool)
// 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()
val hasOnlyOneSection = roomListViewModel.sections.size == 1
roomListViewModel.sections.forEach { section ->
val sectionAdapter = SectionHeaderAdapter {
roomListViewModel.handle(RoomListAction.ToggleSection(section))
}.also {
it.updateSection(SectionHeaderAdapter.SectionViewModel(
section.sectionName
))
}
val contentAdapter = pagedControllerFactory.createRoomSummaryPagedController().also {
section.livePages.observe(viewLifecycleOwner) { pl ->
it.submitList(pl)
sectionAdapter.updateSection(sectionAdapter.section.copy(isHidden = pl.isEmpty() || hasOnlyOneSection))
checkEmptyState()
}
section.notificationCount.observe(viewLifecycleOwner) { counts ->
sectionAdapter.updateSection(sectionAdapter.section.copy(
notificationCount = counts.totalCount(),
isHighlighted = counts.isHighlight()
))
}
section.isExpanded.observe(viewLifecycleOwner) { _ ->
refreshCollapseStates()
}
it.listener = this
}
adapterInfosList.add(
SectionAdapterInfo(
SectionKey(
name = section.sectionName,
isExpanded = section.isExpanded.value.orTrue()
),
sectionAdapter,
contentAdapter
)
)
concatAdapter.addAdapter(sectionAdapter)
concatAdapter.addAdapter(contentAdapter.adapter)
}
// Add the footer controller
this.footerController.listener = this
concatAdapter.addAdapter(footerController.adapter)
this.concatAdapter = concatAdapter
views.roomListView.adapter = concatAdapter
}
private val showFabRunnable = Runnable {
if (isAdded) {
when (roomListParams.displayMode) {
RoomListDisplayMode.NOTIFICATIONS -> views.createChatFabMenu.show()
RoomListDisplayMode.PEOPLE -> views.createChatRoomButton.show()
RoomListDisplayMode.ROOMS -> views.createGroupRoomButton.show()
RoomListDisplayMode.PEOPLE -> views.createChatRoomButton.show()
RoomListDisplayMode.ROOMS -> views.createGroupRoomButton.show()
else -> Unit
}
}
@ -226,28 +320,28 @@ class RoomListFragment @Inject constructor(
private fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) {
when (quickAction) {
is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> {
is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> {
roomListViewModel.handle(RoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.ALL_MESSAGES_NOISY))
}
is RoomListQuickActionsSharedAction.NotificationsAll -> {
is RoomListQuickActionsSharedAction.NotificationsAll -> {
roomListViewModel.handle(RoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.ALL_MESSAGES))
}
is RoomListQuickActionsSharedAction.NotificationsMentionsOnly -> {
roomListViewModel.handle(RoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.MENTIONS_ONLY))
}
is RoomListQuickActionsSharedAction.NotificationsMute -> {
is RoomListQuickActionsSharedAction.NotificationsMute -> {
roomListViewModel.handle(RoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.MUTE))
}
is RoomListQuickActionsSharedAction.Settings -> {
is RoomListQuickActionsSharedAction.Settings -> {
navigator.openRoomProfile(requireActivity(), quickAction.roomId)
}
is RoomListQuickActionsSharedAction.Favorite -> {
is RoomListQuickActionsSharedAction.Favorite -> {
roomListViewModel.handle(RoomListAction.ToggleTag(quickAction.roomId, RoomTag.ROOM_TAG_FAVOURITE))
}
is RoomListQuickActionsSharedAction.LowPriority -> {
is RoomListQuickActionsSharedAction.LowPriority -> {
roomListViewModel.handle(RoomListAction.ToggleTag(quickAction.roomId, RoomTag.ROOM_TAG_LOW_PRIORITY))
}
is RoomListQuickActionsSharedAction.Leave -> {
is RoomListQuickActionsSharedAction.Leave -> {
promptLeaveRoom(quickAction.roomId)
}
}.exhaustive
@ -278,12 +372,7 @@ 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)
footerController.setData(state)
// Mark all as read menu
when (roomListParams.displayMode) {
RoomListDisplayMode.NOTIFICATIONS,
@ -299,68 +388,38 @@ class RoomListFragment @Inject constructor(
}
}
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<RoomSummary>?) {
val hasNoRoom = allRooms
?.filter {
it.membership == Membership.JOIN || it.membership == Membership.INVITE
}
.isNullOrEmpty()
val emptyState = when (roomListParams.displayMode) {
RoomListDisplayMode.NOTIFICATIONS -> {
if (hasNoRoom) {
StateView.State.Empty(
title = getString(R.string.room_list_catchup_welcome_title),
image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_catchup),
message = getString(R.string.room_list_catchup_welcome_body)
)
} else {
private fun checkEmptyState() {
val hasNoRoom = adapterInfosList.all { it.headerHeaderAdapter.section.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 +436,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 +457,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)
}

View File

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

View File

@ -16,37 +16,50 @@
package im.vector.app.features.home.room.list
import androidx.annotation.StringRes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import androidx.paging.PagedList
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.RoomTagQueryFilter
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.RoomCategoryFilter
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
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.rx
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import org.matrix.android.sdk.internal.session.room.UpdatableFilterLivePageResult
import org.matrix.android.sdk.rx.asObservable
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<List<RoomSummary>>)
private val stringProvider: StringProvider)
: VectorViewModel<RoomListViewState, RoomListAction, RoomListViewEvents>(initialState) {
interface Factory {
fun create(initialState: RoomListViewState): RoomListViewModel
}
private var updatableQuery: UpdatableFilterLivePageResult? = null
companion object : MvRxViewModelFactory<RoomListViewModel, RoomListViewState> {
@JvmStatic
@ -56,28 +69,121 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
}
}
private val displayMode = initialState.displayMode
private val roomListDisplayModeFilter = RoomListDisplayModeFilter(displayMode)
data class RoomsSection(
val sectionName: String,
val livePages: LiveData<PagedList<RoomSummary>>,
val isExpanded: MutableLiveData<Boolean> = MutableLiveData(true),
val notificationCount: MutableLiveData<RoomAggregateNotificationCount> =
MutableLiveData(RoomAggregateNotificationCount(0, 0))
)
val sections: List<RoomsSection> by lazy {
val sections = mutableListOf<RoomsSection>()
if (initialState.displayMode == RoomListDisplayMode.PEOPLE) {
addSection(sections, R.string.invitations_header) {
it.memberships = listOf(Membership.INVITE)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
}
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) {
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()
// it.displayName = QueryStringValue.Contains("")
}) { qpm ->
val name = stringProvider.getString(R.string.bottom_action_rooms)
session.getFilteredPagedRoomSummariesLive(qpm)
.let { livePagedList ->
updatableQuery = livePagedList
sections.add(RoomsSection(name, livePagedList.livePagedList))
}
}
}
sections
}
init {
observeRoomSummaries()
observeMembershipChanges()
}
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.SelectRoom -> handleSelectRoom(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.ToggleTag -> handleToggleTag(action)
is RoomListAction.ToggleSection -> handleToggleSection(action.section)
}.exhaustive
}
private fun addSection(sections: MutableList<RoomsSection>, @StringRes nameRes: Int, 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(name, livePagedList))
}
}
}
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 +194,11 @@ 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(section: RoomsSection) {
sections.find { it.sectionName == section.sectionName }
?.let { section ->
section.isExpanded.postValue(!section.isExpanded.value.orFalse())
}
}
private fun handleFilter(action: RoomListAction.FilterWith) {
@ -98,23 +207,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 {
this.memberships = Membership.activeMemberships()
this.displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.INSENSITIVE)
}
)
}
private fun handleAcceptInvitation(action: RoomListAction.AcceptInvitation) = withState { state ->
@ -164,12 +262,12 @@ 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()) }
// 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) {
@ -211,7 +309,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
private fun String.otherTag(): String? {
return when (this) {
RoomTag.ROOM_TAG_FAVOURITE -> RoomTag.ROOM_TAG_LOW_PRIORITY
RoomTag.ROOM_TAG_FAVOURITE -> RoomTag.ROOM_TAG_LOW_PRIORITY
RoomTag.ROOM_TAG_LOW_PRIORITY -> RoomTag.ROOM_TAG_FAVOURITE
else -> null
}
@ -226,46 +324,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<RoomSummary>): RoomSummaries {
// Set up init size on directChats and groupRooms as they are the biggest ones
val invites = ArrayList<RoomSummary>()
val favourites = ArrayList<RoomSummary>()
val directChats = ArrayList<RoomSummary>(rooms.size)
val groupRooms = ArrayList<RoomSummary>(rooms.size)
val lowPriorities = ArrayList<RoomSummary>()
val serverNotices = ArrayList<RoomSummary>()
rooms
.filter { roomListDisplayModeFilter.test(it) }
.forEach { room ->
val tags = room.tags.map { it.name }
when {
room.membership == Membership.INVITE -> invites.add(room)
tags.contains(RoomTag.ROOM_TAG_SERVER_NOTICE) -> serverNotices.add(room)
tags.contains(RoomTag.ROOM_TAG_FAVOURITE) -> favourites.add(room)
tags.contains(RoomTag.ROOM_TAG_LOW_PRIORITY) -> lowPriorities.add(room)
room.isDirect -> directChats.add(room)
else -> groupRooms.add(room)
}
}
return RoomSummaries().apply {
put(RoomCategory.INVITE, invites)
put(RoomCategory.FAVOURITE, favourites)
put(RoomCategory.DIRECT, directChats)
put(RoomCategory.GROUP, groupRooms)
put(RoomCategory.LOW_PRIORITY, lowPriorities)
put(RoomCategory.SERVER_NOTICE, serverNotices)
}
}
}

View File

@ -16,20 +16,20 @@
package im.vector.app.features.home.room.list
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<Session>,
private val homeRoomListDataSource: Provider<HomeRoomListDataSource>)
private val stringProvider: StringProvider)
: RoomListViewModel.Factory {
override fun create(initialState: RoomListViewState): RoomListViewModel {
return RoomListViewModel(
initialState,
session.get(),
homeRoomListDataSource.get()
stringProvider
)
}
}

View File

@ -17,59 +17,26 @@
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<List<RoomSummary>> = Uninitialized,
val roomFilter: String = "",
val asyncFilteredRooms: Async<RoomSummaries> = Uninitialized,
val roomMembershipChanges: Map<String, ChangeMembershipState> = emptyMap(),
val isInviteExpanded: Boolean = true,
val isFavouriteRoomsExpanded: Boolean = true,
val isDirectRoomsExpanded: Boolean = true,
val isGroupRoomsExpanded: Boolean = true,
val isLowPriorityRoomsExpanded: Boolean = true,
val isServerNoticeRoomsExpanded: Boolean = true
val roomMembershipChanges: Map<String, ChangeMembershipState> = 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
val hasUnread: Boolean = false
// get() = asyncFilteredRooms.invoke()
// ?.flatMap { it.value }
// ?.filter { it.membership == Membership.JOIN }
// ?.any { it.hasUnreadMessages }
// ?: false
}
typealias RoomSummaries = LinkedHashMap<RoomCategory, List<RoomSummary>>

View File

@ -1,170 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.list
import androidx.annotation.StringRes
import com.airbnb.epoxy.EpoxyController
import im.vector.app.R
import im.vector.app.core.epoxy.helpFooterItem
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.filtered.FilteredRoomFooterItem
import im.vector.app.features.home.room.filtered.filteredRoomFooterItem
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import javax.inject.Inject
class RoomSummaryController @Inject constructor(private val stringProvider: StringProvider,
private val roomSummaryItemFactory: RoomSummaryItemFactory,
private val roomListNameFilter: RoomListNameFilter,
private val userPreferencesProvider: UserPreferencesProvider
) : EpoxyController() {
var listener: Listener? = null
private var viewState: RoomListViewState? = null
init {
// We are requesting a model build directly as the first build of epoxy is on the main thread.
// It avoids to build the whole list of rooms on the main thread.
requestModelBuild()
}
fun update(viewState: RoomListViewState) {
this.viewState = viewState
requestModelBuild()
}
fun onRoomLongClicked() {
userPreferencesProvider.neverShowLongClickOnRoomHelpAgain()
requestModelBuild()
}
override fun buildModels() {
val nonNullViewState = viewState ?: return
when (nonNullViewState.displayMode) {
RoomListDisplayMode.FILTERED -> buildFilteredRooms(nonNullViewState)
else -> buildRooms(nonNullViewState)
}
}
private fun buildFilteredRooms(viewState: RoomListViewState) {
val summaries = viewState.asyncRooms() ?: return
roomListNameFilter.filter = viewState.roomFilter
val filteredSummaries = summaries
.filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) }
buildRoomModels(filteredSummaries,
viewState.roomMembershipChanges,
emptySet())
addFilterFooter(viewState)
}
private fun buildRooms(viewState: RoomListViewState) {
var showHelp = false
val roomSummaries = viewState.asyncFilteredRooms()
roomSummaries?.forEach { (category, summaries) ->
if (summaries.isEmpty()) {
return@forEach
} else {
val isExpanded = viewState.isCategoryExpanded(category)
buildRoomCategory(viewState, summaries, category.titleRes, viewState.isCategoryExpanded(category)) {
listener?.onToggleRoomCategory(category)
}
if (isExpanded) {
buildRoomModels(summaries,
viewState.roomMembershipChanges,
emptySet())
// Never set showHelp to true for invitation
if (category != RoomCategory.INVITE) {
showHelp = userPreferencesProvider.shouldShowLongClickOnRoomHelp()
}
}
}
}
if (showHelp) {
buildLongClickHelp()
}
}
private fun buildLongClickHelp() {
helpFooterItem {
id("long_click_help")
text(stringProvider.getString(R.string.help_long_click_on_room_for_more_options))
}
}
private fun addFilterFooter(viewState: RoomListViewState) {
filteredRoomFooterItem {
id("filter_footer")
listener(listener)
currentFilter(viewState.roomFilter)
}
}
private fun buildRoomCategory(viewState: RoomListViewState,
summaries: List<RoomSummary>,
@StringRes titleRes: Int,
isExpanded: Boolean,
mutateExpandedState: () -> Unit) {
// TODO should add some business logic later
val unreadCount = if (summaries.isEmpty()) {
0
} else {
summaries.map { it.notificationCount }.sumBy { i -> i }
}
val showHighlighted = summaries.any { it.highlightCount > 0 }
roomCategoryItem {
id(titleRes)
title(stringProvider.getString(titleRes))
expanded(isExpanded)
unreadNotificationCount(unreadCount)
showHighlighted(showHighlighted)
listener {
mutateExpandedState()
update(viewState)
}
}
}
private fun buildRoomModels(summaries: List<RoomSummary>,
roomChangedMembershipStates: Map<String, ChangeMembershipState>,
selectedRoomIds: Set<String>) {
summaries.forEach { roomSummary ->
roomSummaryItemFactory
.create(roomSummary,
roomChangedMembershipStates,
selectedRoomIds,
listener)
.addTo(this)
}
}
interface Listener : FilteredRoomFooterItem.FilteredRoomFooterItemListener {
fun onToggleRoomCategory(roomCategory: RoomCategory)
fun onRoomClicked(room: RoomSummary)
fun onRoomLongClicked(room: RoomSummary): Boolean
fun onRejectRoomInvitation(room: RoomSummary)
fun onAcceptRoomInvitation(room: RoomSummary)
}
}

View File

@ -40,7 +40,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
fun create(roomSummary: RoomSummary,
roomChangeMembershipStates: Map<String, ChangeMembershipState>,
selectedRoomIds: Set<String>,
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 {

View File

@ -0,0 +1,71 @@
/*
* 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.resources.StringProvider
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.core.utils.createUIHandler
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import javax.inject.Inject
class RoomSummaryPagedControllerFactory @Inject constructor(private val stringProvider: StringProvider,
private val userPreferencesProvider: UserPreferencesProvider,
private val roomSummaryItemFactory: RoomSummaryItemFactory) {
fun createRoomSummaryPagedController(): RoomSummaryPagedController {
return RoomSummaryPagedController(stringProvider, userPreferencesProvider, roomSummaryItemFactory)
}
}
class RoomSummaryPagedController constructor(private val stringProvider: StringProvider,
private val userPreferencesProvider: UserPreferencesProvider,
private val roomSummaryItemFactory: RoomSummaryItemFactory)
: PagedListEpoxyController<RoomSummary>(
// Important it must match the PageList builder notify Looper
modelBuildingHandler = createUIHandler()
) {
var listener: RoomListListener? = null
override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> {
val unwrappedItem = item
// for place holder if enabled
?: return roomSummaryItemFactory.createRoomItem(
RoomSummary(
roomId = "null_item_pos_$currentPosition",
name = "",
encryptionEventTs = null,
isEncrypted = false,
typingUsers = emptyList()
), emptySet(), null, null)
// GenericItem_().apply { id("null_item_pos_$currentPosition") }
return roomSummaryItemFactory.create(unwrappedItem, emptyMap(), emptySet(), listener)
}
// override fun onModelBound(holder: EpoxyViewHolder, boundModel: EpoxyModel<*>, position: Int, previouslyBoundModel: EpoxyModel<*>?) {
// Timber.w("VAL: Will load around $position")
// super.onModelBound(holder, boundModel, position, previouslyBoundModel)
// }
// fun onRoomLongClicked() {
// userPreferencesProvider.neverShowLongClickOnRoomHelpAgain()
// requestModelBuild()
// }
}

View File

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