diff --git a/changelog.d/4533.misc b/changelog.d/4533.misc new file mode 100644 index 0000000000..1137a1c43c --- /dev/null +++ b/changelog.d/4533.misc @@ -0,0 +1 @@ +Improve headers UI in Rooms/Messages lists diff --git a/changelog.d/5340.bugfix b/changelog.d/5340.bugfix new file mode 100644 index 0000000000..4c53f0088c --- /dev/null +++ b/changelog.d/5340.bugfix @@ -0,0 +1 @@ +Support both stable and unstable prefixes for Events about Polls and Location \ No newline at end of file diff --git a/changelog.d/5375.wip b/changelog.d/5375.wip new file mode 100644 index 0000000000..352b2385a9 --- /dev/null +++ b/changelog.d/5375.wip @@ -0,0 +1 @@ +Dynamically showing/hiding onboarding personalisation screens based on the users homeserver capabilities \ No newline at end of file diff --git a/changelog.d/5514.bugfix b/changelog.d/5514.bugfix new file mode 100644 index 0000000000..0dfbca6e9a --- /dev/null +++ b/changelog.d/5514.bugfix @@ -0,0 +1 @@ +Read receipt in wrong order \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index 87b8e3c12f..1f2a08b6a6 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -58,6 +58,7 @@ ext.libs = [ 'lifecycleCommon' : "androidx.lifecycle:lifecycle-common:$lifecycle", 'lifecycleLivedata' : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle", 'lifecycleProcess' : "androidx.lifecycle:lifecycle-process:$lifecycle", + 'lifecycleRuntimeKtx' : "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle", 'datastore' : "androidx.datastore:datastore:1.0.0", 'datastorepreferences' : "androidx.datastore:datastore-preferences:1.0.0", 'pagingRuntimeKtx' : "androidx.paging:paging-runtime-ktx:2.1.2", @@ -141,4 +142,4 @@ ext.libs = [ 'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1", 'junit' : "junit:junit:4.13.2" ] -] \ No newline at end of file +] diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 817d666cf8..2ef2dfd91e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -353,7 +353,7 @@ fun Event.isAttachmentMessage(): Boolean { } } -fun Event.isPoll(): Boolean = getClearType() == EventType.POLL_START || getClearType() == EventType.POLL_END +fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START || getClearType() in EventType.POLL_END fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER @@ -376,7 +376,7 @@ fun Event.getRelationContent(): RelationDefaultContent? { * Returns the poll question or null otherwise */ fun Event.getPollQuestion(): String? = - getPollContent()?.pollCreationInfo?.question?.question + getPollContent()?.getBestPollCreationInfo()?.question?.getBestQuestion() /** * Returns the relation content for a specific type or null otherwise diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 0c77b574e7..22fb9bcbe2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -103,9 +103,9 @@ object EventType { const val REACTION = "m.reaction" // Poll - const val POLL_START = "org.matrix.msc3381.poll.start" - const val POLL_RESPONSE = "org.matrix.msc3381.poll.response" - const val POLL_END = "org.matrix.msc3381.poll.end" + val POLL_START = listOf("org.matrix.msc3381.poll.start", "m.poll.start") + val POLL_RESPONSE = listOf("org.matrix.msc3381.poll.response", "m.poll.response") + val POLL_END = listOf("org.matrix.msc3381.poll.end", "m.poll.end") // Unwedging internal const val DUMMY = "m.dummy" 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 bca432320d..f506b147df 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 @@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.room import androidx.lifecycle.LiveData import androidx.paging.PagedList +import kotlinx.coroutines.flow.Flow import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.Membership @@ -216,6 +217,11 @@ interface RoomService { pagedListConfig: PagedList.Config = defaultPagedListConfig, sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): UpdatableLivePageResult + /** + * Retrieve a flow on the number of rooms. + */ + fun getRoomCountFlow(queryParams: RoomSummaryQueryParams): Flow + /** * TODO Doc */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt index b83f57f5ef..db87f913b9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt @@ -22,10 +22,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary interface UpdatableLivePageResult { val livePagedList: LiveData> - - fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams) - val liveBoundaries: LiveData + var queryParams: RoomSummaryQueryParams } data class ResultBoundaries( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt index d07bd2d73a..84bf5cf7b7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt @@ -39,37 +39,46 @@ data class MessageLocationContent( */ @Json(name = "geo_uri") val geoUri: String, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null, /** * See https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md */ - @Json(name = "org.matrix.msc3488.location") val locationInfo: LocationInfo? = null, - - @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, - @Json(name = "m.new_content") override val newContent: Content? = null, - + @Json(name = "org.matrix.msc3488.location") val unstableLocationInfo: LocationInfo? = null, + @Json(name = "m.location") val locationInfo: LocationInfo? = null, + /** + * Exact time that the data in the event refers to (milliseconds since the UNIX epoch) + */ + @Json(name = "org.matrix.msc3488.ts") val unstableTs: Long? = null, + @Json(name = "m.ts") val ts: Long? = null, + @Json(name = "org.matrix.msc1767.text") val unstableText: String? = null, + @Json(name = "m.text") val text: String? = null, /** * m.asset defines a generic asset that can be used for location tracking but also in other places like * inventories, geofencing, checkins/checkouts etc. * It should contain a mandatory namespaced type key defining what particular asset is being referred to. * For the purposes of user location tracking m.self should be used in order to avoid duplicating the mxid. */ - @Json(name = "m.asset") val locationAsset: LocationAsset? = null, - - /** - * Exact time that the data in the event refers to (milliseconds since the UNIX epoch) - */ - @Json(name = "org.matrix.msc3488.ts") val ts: Long? = null, - - @Json(name = "org.matrix.msc1767.text") val text: String? = null + @Json(name = "org.matrix.msc3488.asset") val unstableLocationAsset: LocationAsset? = null, + @Json(name = "m.asset") val locationAsset: LocationAsset? = null ) : MessageContent { - fun getBestGeoUri() = locationInfo?.geoUri ?: geoUri + fun getBestLocationInfo() = locationInfo ?: unstableLocationInfo + + fun getBestTs() = ts ?: unstableTs + + fun getBestText() = text ?: unstableText + + fun getBestLocationAsset() = locationAsset ?: unstableLocationAsset + + fun getBestGeoUri() = getBestLocationInfo()?.geoUri ?: geoUri /** * @return true if the location asset is a user location, not a generic one. */ fun isSelfLocation(): Boolean { // Should behave like m.self if locationAsset is null + val locationAsset = getBestLocationAsset() return locationAsset?.type == null || locationAsset.type == LocationAssetType.SELF } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt index a4e1317290..43c0c90068 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt @@ -31,5 +31,9 @@ data class MessagePollContent( @Json(name = "body") override val body: String = "", @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null, - @Json(name = "org.matrix.msc3381.poll.start") val pollCreationInfo: PollCreationInfo? = null -) : MessageContent + @Json(name = "org.matrix.msc3381.poll.start") val unstablePollCreationInfo: PollCreationInfo? = null, + @Json(name = "m.poll.start") val pollCreationInfo: PollCreationInfo? = null +) : MessageContent { + + fun getBestPollCreationInfo() = pollCreationInfo ?: unstablePollCreationInfo +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt index f3b4e3dc23..022915ed69 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt @@ -31,5 +31,9 @@ data class MessagePollResponseContent( @Json(name = "body") override val body: String = "", @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.new_content") override val newContent: Content? = null, - @Json(name = "org.matrix.msc3381.poll.response") val response: PollResponse? = null -) : MessageContent + @Json(name = "org.matrix.msc3381.poll.response") val unstableResponse: PollResponse? = null, + @Json(name = "m.response") val response: PollResponse? = null +) : MessageContent { + + fun getBestResponse() = response ?: unstableResponse +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt index 8f5ff53c85..34614d9d15 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt @@ -22,5 +22,9 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class PollAnswer( @Json(name = "id") val id: String? = null, - @Json(name = "org.matrix.msc1767.text") val answer: String? = null -) + @Json(name = "org.matrix.msc1767.text") val unstableAnswer: String? = null, + @Json(name = "m.text") val answer: String? = null +) { + + fun getBestAnswer() = answer ?: unstableAnswer +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt index a82c01b159..81b034a809 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt @@ -21,8 +21,8 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class PollCreationInfo( - @Json(name = "question") val question: PollQuestion? = null, - @Json(name = "kind") val kind: PollType? = PollType.DISCLOSED, - @Json(name = "max_selections") val maxSelections: Int = 1, - @Json(name = "answers") val answers: List? = null + @Json(name = "question") val question: PollQuestion? = null, + @Json(name = "kind") val kind: PollType? = PollType.DISCLOSED_UNSTABLE, + @Json(name = "max_selections") val maxSelections: Int = 1, + @Json(name = "answers") val answers: List? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt index 76025f745e..df9517892b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt @@ -21,5 +21,9 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class PollQuestion( - @Json(name = "org.matrix.msc1767.text") val question: String? = null -) + @Json(name = "org.matrix.msc1767.text") val unstableQuestion: String? = null, + @Json(name = "m.text") val question: String? = null +) { + + fun getBestQuestion() = question ?: unstableQuestion +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt index 3a8066b9bc..54801e698d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt @@ -25,11 +25,17 @@ enum class PollType { * Voters should see results as soon as they have voted. */ @Json(name = "org.matrix.msc3381.poll.disclosed") + DISCLOSED_UNSTABLE, + + @Json(name = "m.poll.disclosed") DISCLOSED, /** * Results should be only revealed when the poll is ended. */ @Json(name = "org.matrix.msc3381.poll.undisclosed") + UNDISCLOSED_UNSTABLE, + + @Json(name = "m.poll.undisclosed") UNDISCLOSED } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt index 3bba2deae5..eaed9053ea 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt @@ -32,7 +32,6 @@ object RoomSummaryConstants { EventType.CALL_ANSWER, EventType.ENCRYPTED, EventType.STICKER, - EventType.REACTION, - EventType.POLL_START - ) + EventType.REACTION + ) + EventType.POLL_START } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index c03d0fd17b..01686ca04b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -135,9 +135,9 @@ fun TimelineEvent.getEditedEventId(): String? { */ fun TimelineEvent.getLastMessageContent(): MessageContent? { return when (root.getClearType()) { - EventType.STICKER -> root.getClearContent().toModel() - EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() - else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() + EventType.STICKER -> root.getClearContent().toModel() + in EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() + else -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt index 8b05d2ea62..8ae203c2b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt @@ -56,7 +56,7 @@ internal class DefaultProcessEventForPushTask @Inject constructor( val allEvents = (newJoinEvents + inviteEvents).filter { event -> when (event.type) { - EventType.POLL_START, + in EventType.POLL_START, EventType.MESSAGE, EventType.REDACTION, EventType.ENCRYPTED, 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 4a02c55db0..0d78489fbd 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 @@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy +import kotlinx.coroutines.flow.Flow 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 @@ -109,6 +110,10 @@ internal class DefaultRoomService @Inject constructor( return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder) } + override fun getRoomCountFlow(queryParams: RoomSummaryQueryParams): Flow { + return roomSummaryDataSource.getCountFlow(queryParams) + } + override fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount { return roomSummaryDataSource.getNotificationCountForRooms(queryParams) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index e89f99994b..4a43cfc22a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -87,11 +87,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor( // TODO Add ? // EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_KEY, - EventType.ENCRYPTED, - EventType.POLL_START, - EventType.POLL_RESPONSE, - EventType.POLL_END - ) + EventType.ENCRYPTED + ) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { return allowedTypes.contains(eventType) @@ -157,7 +154,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( Timber.v("###REPLACE in room $roomId for event ${event.eventId}") // A replace! handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) - } else if (event.getClearType() == EventType.POLL_RESPONSE) { + } else if (event.getClearType() in EventType.POLL_RESPONSE) { event.getClearContent().toModel(catchError = true)?.let { pollResponseContent -> Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") handleResponse(realm, event, pollResponseContent, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) @@ -178,12 +175,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor( handleVerification(realm, event, roomId, isLocalEcho, it) } } - EventType.POLL_RESPONSE -> { + in EventType.POLL_RESPONSE -> { event.getClearContent().toModel(catchError = true)?.let { handleResponse(realm, event, it, roomId, isLocalEcho, event.getRelationContent()?.eventId) } } - EventType.POLL_END -> { + in EventType.POLL_END -> { event.content.toModel(catchError = true)?.let { handleEndPoll(realm, event, it, roomId, isLocalEcho) } @@ -228,7 +225,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } } } - EventType.POLL_START -> { + in EventType.POLL_START -> { val content: MessagePollContent? = event.content.toModel() if (content?.relatesTo?.type == RelationType.REPLACE) { Timber.v("###REPLACE in room $roomId for event ${event.eventId}") @@ -236,12 +233,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor( handleReplace(realm, event, content, roomId, isLocalEcho) } } - EventType.POLL_RESPONSE -> { + in EventType.POLL_RESPONSE -> { event.content.toModel(catchError = true)?.let { handleResponse(realm, event, it, roomId, isLocalEcho) } } - EventType.POLL_END -> { + in EventType.POLL_END -> { event.content.toModel(catchError = true)?.let { handleEndPoll(realm, event, it, roomId, isLocalEcho) } @@ -423,12 +420,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor( return } - val option = content.response?.answers?.first() ?: return Unit.also { + val option = content.getBestResponse()?.answers?.first() ?: return Unit.also { Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}") } // Check if this option is in available options - if (!targetPollContent.pollCreationInfo?.answers?.map { it.id }?.contains(option).orFalse()) { + if (!targetPollContent.getBestPollCreationInfo()?.answers?.map { it.id }?.contains(option).orFalse()) { Timber.v("## POLL $targetEventId doesn't contain option $option") return } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt index ee52fe574b..4753e12157 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt @@ -71,7 +71,7 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr when (typeToPrune) { EventType.ENCRYPTED, EventType.MESSAGE, - EventType.POLL_START -> { + in EventType.POLL_START -> { Timber.d("REDACTION for message ${eventToPrune.eventId}") val unsignedData = EventMapper.map(eventToPrune).unsignedData ?: UnsignedData(null, null) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 3c23cd1869..bec0ce97dc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -137,16 +137,11 @@ internal class LocalEchoEventFactory @Inject constructor( options: List, pollType: PollType): MessagePollContent { return MessagePollContent( - pollCreationInfo = PollCreationInfo( - question = PollQuestion( - question = question - ), + unstablePollCreationInfo = PollCreationInfo( + question = PollQuestion(unstableQuestion = question), kind = pollType, answers = options.map { option -> - PollAnswer( - id = UUID.randomUUID().toString(), - answer = option - ) + PollAnswer(id = UUID.randomUUID().toString(), unstableAnswer = option) } ) ) @@ -167,7 +162,7 @@ internal class LocalEchoEventFactory @Inject constructor( originServerTs = dummyOriginServerTs(), senderId = userId, eventId = localId, - type = EventType.POLL_START, + type = EventType.POLL_START.first(), content = newContent.toContent() ) } @@ -179,11 +174,9 @@ internal class LocalEchoEventFactory @Inject constructor( body = answerId, relatesTo = RelationDefaultContent( type = RelationType.REFERENCE, - eventId = pollEventId), - response = PollResponse( - answers = listOf(answerId) - ) - + eventId = pollEventId + ), + unstableResponse = PollResponse(answers = listOf(answerId)) ) val localId = LocalEcho.createLocalEchoId() return Event( @@ -191,7 +184,7 @@ internal class LocalEchoEventFactory @Inject constructor( originServerTs = dummyOriginServerTs(), senderId = userId, eventId = localId, - type = EventType.POLL_RESPONSE, + type = EventType.POLL_RESPONSE.first(), content = content.toContent(), unsignedData = UnsignedData(age = null, transactionId = localId)) } @@ -207,7 +200,7 @@ internal class LocalEchoEventFactory @Inject constructor( originServerTs = dummyOriginServerTs(), senderId = userId, eventId = localId, - type = EventType.POLL_START, + type = EventType.POLL_START.first(), content = content.toContent(), unsignedData = UnsignedData(age = null, transactionId = localId)) } @@ -226,7 +219,7 @@ internal class LocalEchoEventFactory @Inject constructor( originServerTs = dummyOriginServerTs(), senderId = userId, eventId = localId, - type = EventType.POLL_END, + type = EventType.POLL_END.first(), content = content.toContent(), unsignedData = UnsignedData(age = null, transactionId = localId)) } @@ -239,15 +232,10 @@ internal class LocalEchoEventFactory @Inject constructor( val content = MessageLocationContent( geoUri = geoUri, body = geoUri, - locationInfo = LocationInfo( - geoUri = geoUri, - description = geoUri - ), - locationAsset = LocationAsset( - type = LocationAssetType.SELF - ), - ts = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()), - text = geoUri + unstableLocationInfo = LocationInfo(geoUri = geoUri, description = geoUri), + unstableLocationAsset = LocationAsset(type = LocationAssetType.SELF), + unstableTs = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()), + unstableText = geoUri ) return createMessageEvent(roomId, content) } @@ -644,7 +632,9 @@ internal class LocalEchoEventFactory @Inject constructor( MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.") MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.") MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.") - MessageType.MSGTYPE_POLL_START -> return TextContent((content as? MessagePollContent)?.pollCreationInfo?.question?.question ?: "") + MessageType.MSGTYPE_POLL_START -> { + return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "") + } else -> return TextContent(content?.body ?: "") } } 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 c9fc3c9575..ea4f102fa5 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 @@ -25,7 +25,13 @@ import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy import io.realm.Realm import io.realm.RealmQuery +import io.realm.kotlin.toFlow import io.realm.kotlin.where +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.query.isNormalized @@ -42,6 +48,7 @@ import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotification import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields @@ -55,8 +62,10 @@ import javax.inject.Inject internal class RoomSummaryDataSource @Inject constructor( @SessionDatabase private val monarchy: Monarchy, + private val realmSessionProvider: RealmSessionProvider, private val roomSummaryMapper: RoomSummaryMapper, - private val queryStringValueProcessor: QueryStringValueProcessor + private val queryStringValueProcessor: QueryStringValueProcessor, + private val coroutineDispatchers: MatrixCoroutineDispatchers ) { fun getRoomSummary(roomIdOrAlias: String): RoomSummary? { @@ -219,17 +228,29 @@ internal class RoomSummaryDataSource @Inject constructor( return object : UpdatableLivePageResult { override val livePagedList: LiveData> = mapped - override fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams) { - realmDataSourceFactory.updateQuery { - roomSummariesQuery(it, builder.invoke(queryParams)).process(sortOrder) - } - } - override val liveBoundaries: LiveData get() = boundaries + + override var queryParams: RoomSummaryQueryParams = queryParams + set(value) { + field = value + realmDataSourceFactory.updateQuery { + roomSummariesQuery(it, value).process(sortOrder) + } + } } } + fun getCountFlow(queryParams: RoomSummaryQueryParams): Flow = + realmSessionProvider + .withRealm { realm -> roomSummariesQuery(realm, queryParams).findAllAsync() } + .toFlow() + // need to create the flow on a context dispatcher with a thread with attached Looper + .flowOn(coroutineDispatchers.main) + .map { it.size } + .flowOn(coroutineDispatchers.io) + .distinctUntilChanged() + fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount { var notificationCount: RoomAggregateNotificationCount? = null monarchy.doWithRealm { realm -> diff --git a/vector/build.gradle b/vector/build.gradle index 2b37c12323..2d9c097da8 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -355,6 +355,7 @@ dependencies { // Lifecycle implementation libs.androidx.lifecycleLivedata implementation libs.androidx.lifecycleProcess + implementation libs.androidx.lifecycleRuntimeKtx implementation libs.androidx.datastore implementation libs.androidx.datastorepreferences diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorOverrides.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorOverrides.kt index 4394f5436e..5e16182f3c 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorOverrides.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorOverrides.kt @@ -22,13 +22,17 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.preferencesDataStore +import im.vector.app.features.HomeserverCapabilitiesOverride import im.vector.app.features.VectorOverrides +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import org.matrix.android.sdk.api.extensions.orFalse private val Context.dataStore: DataStore by preferencesDataStore(name = "vector_overrides") private val keyForceDialPadDisplay = booleanPreferencesKey("force_dial_pad_display") private val keyForceLoginFallback = booleanPreferencesKey("force_login_fallback") +private val forceCanChangeDisplayName = booleanPreferencesKey("force_can_change_display_name") +private val forceCanChangeAvatar = booleanPreferencesKey("force_can_change_avatar") class DebugVectorOverrides(private val context: Context) : VectorOverrides { @@ -40,6 +44,13 @@ class DebugVectorOverrides(private val context: Context) : VectorOverrides { preferences[keyForceLoginFallback].orFalse() } + override val forceHomeserverCapabilities = context.dataStore.data.map { preferences -> + HomeserverCapabilitiesOverride( + canChangeDisplayName = preferences[forceCanChangeDisplayName], + canChangeAvatar = preferences[forceCanChangeAvatar] + ) + } + suspend fun setForceDialPadDisplay(force: Boolean) { context.dataStore.edit { settings -> settings[keyForceDialPadDisplay] = force @@ -51,4 +62,18 @@ class DebugVectorOverrides(private val context: Context) : VectorOverrides { settings[keyForceLoginFallback] = force } } + + suspend fun setHomeserverCapabilities(block: HomeserverCapabilitiesOverride.() -> HomeserverCapabilitiesOverride) { + val capabilitiesOverride = block(forceHomeserverCapabilities.firstOrNull() ?: HomeserverCapabilitiesOverride(null, null)) + context.dataStore.edit { settings -> + when (capabilitiesOverride.canChangeDisplayName) { + null -> settings.remove(forceCanChangeDisplayName) + else -> settings[forceCanChangeDisplayName] = capabilitiesOverride.canChangeDisplayName + } + when (capabilitiesOverride.canChangeAvatar) { + null -> settings.remove(forceCanChangeAvatar) + else -> settings[forceCanChangeAvatar] = capabilitiesOverride.canChangeAvatar + } + } + } } diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt index b54d776901..38253fe7c2 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsFragment.kt @@ -50,6 +50,12 @@ class DebugPrivateSettingsFragment : VectorBaseFragment + viewModel.handle(DebugPrivateSettingsViewActions.SetDisplayNameCapabilityOverride(option)) + } + views.forceChangeAvatarCapability.bind(it.homeserverCapabilityOverrides.avatar) { option -> + viewModel.handle(DebugPrivateSettingsViewActions.SetAvatarCapabilityOverride(option)) + } views.forceLoginFallback.isChecked = it.forceLoginFallback } } diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt index 1c76cf6fb2..5dea3dce64 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewActions.kt @@ -18,7 +18,9 @@ package im.vector.app.features.debug.settings import im.vector.app.core.platform.VectorViewModelAction -sealed class DebugPrivateSettingsViewActions : VectorViewModelAction { - data class SetDialPadVisibility(val force: Boolean) : DebugPrivateSettingsViewActions() - data class SetForceLoginFallbackEnabled(val force: Boolean) : DebugPrivateSettingsViewActions() +sealed interface DebugPrivateSettingsViewActions : VectorViewModelAction { + data class SetDialPadVisibility(val force: Boolean) : DebugPrivateSettingsViewActions + data class SetForceLoginFallbackEnabled(val force: Boolean) : DebugPrivateSettingsViewActions + data class SetDisplayNameCapabilityOverride(val option: BooleanHomeserverCapabilitiesOverride?) : DebugPrivateSettingsViewActions + data class SetAvatarCapabilityOverride(val option: BooleanHomeserverCapabilitiesOverride?) : DebugPrivateSettingsViewActions } diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt index 8d040d4773..62871023bc 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewModel.kt @@ -22,9 +22,12 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.debug.features.DebugVectorOverrides +import im.vector.app.features.debug.settings.DebugPrivateSettingsViewActions.SetAvatarCapabilityOverride +import im.vector.app.features.debug.settings.DebugPrivateSettingsViewActions.SetDisplayNameCapabilityOverride import kotlinx.coroutines.launch class DebugPrivateSettingsViewModel @AssistedInject constructor( @@ -40,10 +43,10 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { - observeVectorDataStore() + observeVectorOverrides() } - private fun observeVectorDataStore() { + private fun observeVectorOverrides() { debugVectorOverrides.forceDialPad.setOnEach { copy( dialPadVisible = it @@ -52,13 +55,23 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor( debugVectorOverrides.forceLoginFallback.setOnEach { copy(forceLoginFallback = it) } + debugVectorOverrides.forceHomeserverCapabilities.setOnEach { + val activeDisplayNameOption = BooleanHomeserverCapabilitiesOverride.from(it.canChangeDisplayName) + val activeAvatarOption = BooleanHomeserverCapabilitiesOverride.from(it.canChangeAvatar) + copy(homeserverCapabilityOverrides = homeserverCapabilityOverrides.copy( + displayName = homeserverCapabilityOverrides.displayName.copy(activeOption = activeDisplayNameOption), + avatar = homeserverCapabilityOverrides.avatar.copy(activeOption = activeAvatarOption), + )) + } } override fun handle(action: DebugPrivateSettingsViewActions) { when (action) { is DebugPrivateSettingsViewActions.SetDialPadVisibility -> handleSetDialPadVisibility(action) is DebugPrivateSettingsViewActions.SetForceLoginFallbackEnabled -> handleSetForceLoginFallbackEnabled(action) - } + is SetDisplayNameCapabilityOverride -> handSetDisplayNameCapabilityOverride(action) + is SetAvatarCapabilityOverride -> handSetAvatarCapabilityOverride(action) + }.exhaustive } private fun handleSetDialPadVisibility(action: DebugPrivateSettingsViewActions.SetDialPadVisibility) { @@ -72,4 +85,18 @@ class DebugPrivateSettingsViewModel @AssistedInject constructor( debugVectorOverrides.setForceLoginFallback(action.force) } } + + private fun handSetDisplayNameCapabilityOverride(action: SetDisplayNameCapabilityOverride) { + viewModelScope.launch { + val forceDisplayName = action.option.toBoolean() + debugVectorOverrides.setHomeserverCapabilities { copy(canChangeDisplayName = forceDisplayName) } + } + } + + private fun handSetAvatarCapabilityOverride(action: SetAvatarCapabilityOverride) { + viewModelScope.launch { + val forceAvatar = action.option.toBoolean() + debugVectorOverrides.setHomeserverCapabilities { copy(canChangeAvatar = forceAvatar) } + } + } } diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt index 7fca29af8c..749b11a744 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/settings/DebugPrivateSettingsViewState.kt @@ -17,8 +17,23 @@ package im.vector.app.features.debug.settings import com.airbnb.mvrx.MavericksState +import im.vector.app.features.debug.settings.OverrideDropdownView.OverrideDropdown data class DebugPrivateSettingsViewState( val dialPadVisible: Boolean = false, val forceLoginFallback: Boolean = false, + val homeserverCapabilityOverrides: HomeserverCapabilityOverrides = HomeserverCapabilityOverrides() ) : MavericksState + +data class HomeserverCapabilityOverrides( + val displayName: OverrideDropdown = OverrideDropdown( + label = "Override display name capability", + activeOption = null, + options = listOf(BooleanHomeserverCapabilitiesOverride.ForceEnabled, BooleanHomeserverCapabilitiesOverride.ForceDisabled) + ), + val avatar: OverrideDropdown = OverrideDropdown( + label = "Override avatar capability", + activeOption = null, + options = listOf(BooleanHomeserverCapabilitiesOverride.ForceEnabled, BooleanHomeserverCapabilitiesOverride.ForceDisabled) + ) +) diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/OverrideDropdownView.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/OverrideDropdownView.kt new file mode 100644 index 0000000000..48ec44f909 --- /dev/null +++ b/vector/src/debug/java/im/vector/app/features/debug/settings/OverrideDropdownView.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2022 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.debug.settings + +import android.content.Context +import android.util.AttributeSet +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.LinearLayout +import im.vector.app.R +import im.vector.app.databinding.ViewBooleanDropdownBinding + +class OverrideDropdownView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : LinearLayout(context, attrs) { + + private val binding = ViewBooleanDropdownBinding.inflate( + LayoutInflater.from(context), + this + ) + + init { + orientation = HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + } + + fun bind(feature: OverrideDropdown, listener: Listener) { + binding.overrideLabel.text = feature.label + + binding.overrideOptions.apply { + val arrayAdapter = ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item) + val options = listOf("Inactive") + feature.options.map { it.label } + arrayAdapter.addAll(options) + adapter = arrayAdapter + + feature.activeOption?.let { + setSelection(options.indexOf(it.label), false) + } + + onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + when (position) { + 0 -> listener.onOverrideSelected(option = null) + else -> listener.onOverrideSelected(feature.options[position - 1]) + } + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + // do nothing + } + } + } + } + + fun interface Listener { + fun onOverrideSelected(option: T?) + } + + data class OverrideDropdown( + val label: String, + val options: List, + val activeOption: T?, + ) +} + +interface OverrideOption { + val label: String +} diff --git a/vector/src/debug/java/im/vector/app/features/debug/settings/PrivateSettingOverrides.kt b/vector/src/debug/java/im/vector/app/features/debug/settings/PrivateSettingOverrides.kt new file mode 100644 index 0000000000..316e8fb901 --- /dev/null +++ b/vector/src/debug/java/im/vector/app/features/debug/settings/PrivateSettingOverrides.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 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.debug.settings + +sealed interface BooleanHomeserverCapabilitiesOverride : OverrideOption { + + companion object { + fun from(value: Boolean?) = when (value) { + null -> null + true -> ForceEnabled + false -> ForceDisabled + } + } + + object ForceEnabled : BooleanHomeserverCapabilitiesOverride { + override val label = "Force enabled" + } + + object ForceDisabled : BooleanHomeserverCapabilitiesOverride { + override val label = "Force disabled" + } +} + +fun BooleanHomeserverCapabilitiesOverride?.toBoolean() = when (this) { + null -> null + BooleanHomeserverCapabilitiesOverride.ForceDisabled -> false + BooleanHomeserverCapabilitiesOverride.ForceEnabled -> true +} diff --git a/vector/src/debug/res/layout/fragment_debug_private_settings.xml b/vector/src/debug/res/layout/fragment_debug_private_settings.xml index 6760c68169..c42ad68dce 100644 --- a/vector/src/debug/res/layout/fragment_debug_private_settings.xml +++ b/vector/src/debug/res/layout/fragment_debug_private_settings.xml @@ -31,6 +31,24 @@ android:layout_height="wrap_content" android:text="Force login and registration fallback" /> + + + + diff --git a/vector/src/debug/res/layout/view_boolean_dropdown.xml b/vector/src/debug/res/layout/view_boolean_dropdown.xml new file mode 100644 index 0000000000..5018d61047 --- /dev/null +++ b/vector/src/debug/res/layout/view_boolean_dropdown.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt b/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt index b1df29ebc6..28c1587b1a 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/TimelineEvent.kt @@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent fun TimelineEvent.canReact(): Boolean { // Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment - return root.getClearType() in listOf(EventType.MESSAGE, EventType.STICKER, EventType.POLL_START) && + return root.getClearType() in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START && root.sendState == SendState.SYNCED && !root.isRedacted() } diff --git a/vector/src/main/java/im/vector/app/features/DefaultVectorOverrides.kt b/vector/src/main/java/im/vector/app/features/DefaultVectorOverrides.kt index 4128fdbe3c..daa0d9e0bd 100644 --- a/vector/src/main/java/im/vector/app/features/DefaultVectorOverrides.kt +++ b/vector/src/main/java/im/vector/app/features/DefaultVectorOverrides.kt @@ -22,9 +22,16 @@ import kotlinx.coroutines.flow.flowOf interface VectorOverrides { val forceDialPad: Flow val forceLoginFallback: Flow + val forceHomeserverCapabilities: Flow? } +data class HomeserverCapabilitiesOverride( + val canChangeDisplayName: Boolean?, + val canChangeAvatar: Boolean? +) + class DefaultVectorOverrides : VectorOverrides { override val forceDialPad = flowOf(false) override val forceLoginFallback = flowOf(false) + override val forceHomeserverCapabilities: Flow? = null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 662af3d546..c638666cd7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1189,7 +1189,7 @@ class TimelineFragment @Inject constructor( val formattedDuration = DateUtils.formatElapsedTime(((messageContent.audioInfo?.duration ?: 0) / 1000).toLong()) getString(R.string.voice_message_reply_content, formattedDuration) } else if (messageContent is MessagePollContent) { - messageContent.pollCreationInfo?.question?.question + messageContent.getBestPollCreationInfo()?.question?.getBestQuestion() } else { messageContent?.body ?: "" } @@ -2165,7 +2165,7 @@ class TimelineFragment @Inject constructor( timelineViewModel.handle(RoomDetailAction.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) } is EventSharedAction.Edit -> { - if (action.eventType == EventType.POLL_START) { + if (action.eventType in EventType.POLL_START) { navigator.openCreatePoll(requireContext(), timelineArgs.roomId, action.eventId, PollMode.EDIT) } else if (withState(messageComposerViewModel) { it.isVoiceMessageIdle }) { messageComposerViewModel.handle(MessageComposerAction.EnterEditMode(action.eventId, views.composerLayout.text.toString())) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 5575d9b7f6..b99dbcc220 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -181,7 +181,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } else { when (timelineEvent.root.getClearType()) { EventType.MESSAGE, - EventType.STICKER -> { + EventType.STICKER -> { val messageContent: MessageContent? = timelineEvent.getLastMessageContent() if (messageContent is MessageTextContent && messageContent.format == MessageFormat.FORMAT_MATRIX_HTML) { val html = messageContent.formattedBody @@ -207,13 +207,14 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted EventType.CALL_INVITE, EventType.CALL_CANDIDATES, EventType.CALL_HANGUP, - EventType.CALL_ANSWER -> { + EventType.CALL_ANSWER -> { noticeEventFormatter.format(timelineEvent, room?.roomSummary()?.isDirect.orFalse()) } - EventType.POLL_START -> { - timelineEvent.root.getClearContent().toModel(catchError = true)?.pollCreationInfo?.question?.question ?: "" + in EventType.POLL_START -> { + timelineEvent.root.getClearContent().toModel(catchError = true) + ?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "" } - else -> null + else -> null } } } catch (failure: Throwable) { @@ -373,7 +374,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } if (canRedact(timelineEvent, actionPermissions)) { - if (timelineEvent.root.getClearType() == EventType.POLL_START) { + if (timelineEvent.root.getClearType() in EventType.POLL_START) { add(EventSharedAction.Redact( eventId, askForReason = informationData.senderId != session.myUserId, @@ -425,7 +426,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted private fun canReply(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean { // Only EventType.MESSAGE and EventType.POLL_START event types are supported for the moment - if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.POLL_START)) return false + if (event.root.getClearType() !in EventType.POLL_START + EventType.MESSAGE) return false if (!actionPermissions.canSendMessage) return false return when (messageContent?.msgType) { MessageType.MSGTYPE_TEXT, @@ -511,7 +512,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted private fun canRedact(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { // Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment - if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.POLL_START)) return false + if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START) return false // Message sent by the current user can always be redacted if (event.root.senderId == session.myUserId) return true // Check permission for messages sent by other users @@ -526,13 +527,13 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted private fun canViewReactions(event: TimelineEvent): Boolean { // Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment - if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.POLL_START)) return false + if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START) return false return event.annotations?.reactionsSummary?.isNotEmpty() ?: false } private fun canEdit(event: TimelineEvent, myUserId: String, actionPermissions: ActionPermissions): Boolean { // Only event of type EventType.MESSAGE and EventType.POLL_START are supported for the moment - if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.POLL_START)) return false + if (event.root.getClearType() !in listOf(EventType.MESSAGE) + EventType.POLL_START) return false if (!actionPermissions.canSendMessage) return false // TODO if user is admin or moderator val messageContent = event.root.getClearContent().toModel() @@ -578,13 +579,13 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } private fun canEndPoll(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { - return event.root.getClearType() == EventType.POLL_START && + return event.root.getClearType() in EventType.POLL_START && canRedact(event, actionPermissions) && event.annotations?.pollResponseSummary?.closedTime == null } private fun canEditPoll(event: TimelineEvent): Boolean { - return event.root.getClearType() == EventType.POLL_START && + return event.root.getClearType() in EventType.POLL_START && event.annotations?.pollResponseSummary?.closedTime == null && event.annotations?.pollResponseSummary?.aggregatedContent?.totalVotes ?: 0 == 0 } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 2890e070ef..659cef2b37 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -247,7 +247,7 @@ class MessageItemFactory @Inject constructor( val didUserVoted = pollResponseSummary?.myVote?.isNotEmpty().orFalse() val winnerVoteCount = pollResponseSummary?.winnerVoteCount val isPollSent = informationData.sendState.isSent() - val isPollUndisclosed = pollContent.pollCreationInfo?.kind == PollType.UNDISCLOSED + val isPollUndisclosed = pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED_UNSTABLE val totalVotesText = (pollResponseSummary?.totalVotes ?: 0).let { when { @@ -262,13 +262,13 @@ class MessageItemFactory @Inject constructor( } } - pollContent.pollCreationInfo?.answers?.forEach { option -> + pollContent.getBestPollCreationInfo()?.answers?.forEach { option -> val voteSummary = pollResponseSummary?.votes?.get(option.id) val isMyVote = pollResponseSummary?.myVote == option.id val voteCount = voteSummary?.total ?: 0 val votePercentage = voteSummary?.percentage ?: 0.0 val optionId = option.id ?: "" - val optionAnswer = option.answer ?: "" + val optionAnswer = option.getBestAnswer() ?: "" optionViewStates.add( if (!isPollSent) { @@ -291,7 +291,7 @@ class MessageItemFactory @Inject constructor( ) } - val question = pollContent.pollCreationInfo?.question?.question ?: "" + val question = pollContent.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "" return PollItem_() .attributes(attributes) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt index e66dd4b043..ed3cc8df53 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt @@ -38,8 +38,7 @@ class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: Av .map { ReadReceiptData(it.roomMember.userId, it.roomMember.avatarUrl, it.roomMember.displayName, it.originServerTs) } - .toList() - + .sortedByDescending { it.timestamp } return ReadReceiptsItem_() .id("read_receipts_$eventId") .eventId(eventId) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index b41e1d8f25..f9d2613e27 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -94,7 +94,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me when (event.root.getClearType()) { // Message itemsX EventType.STICKER, - EventType.POLL_START, + in EventType.POLL_START, EventType.MESSAGE -> messageItemFactory.create(params) EventType.REDACTION, EventType.KEY_VERIFICATION_ACCEPT, @@ -107,8 +107,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me EventType.CALL_SELECT_ANSWER, EventType.CALL_NEGOTIATE, EventType.REACTION, - EventType.POLL_RESPONSE, - EventType.POLL_END -> noticeItemFactory.create(params) + in EventType.POLL_RESPONSE, + in EventType.POLL_END -> noticeItemFactory.create(params) // Calls EventType.CALL_INVITE, EventType.CALL_HANGUP, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index d4a6f2ee87..29e12de228 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -122,14 +122,14 @@ class DisplayableEventFormatter @Inject constructor( EventType.CALL_CANDIDATES -> { span { } } - EventType.POLL_START -> { - timelineEvent.root.getClearContent().toModel(catchError = true)?.pollCreationInfo?.question?.question + in EventType.POLL_START -> { + timelineEvent.root.getClearContent().toModel(catchError = true)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: stringProvider.getString(R.string.sent_a_poll) } - EventType.POLL_RESPONSE -> { + in EventType.POLL_RESPONSE -> { stringProvider.getString(R.string.poll_response_room_list_preview) } - EventType.POLL_END -> { + in EventType.POLL_END -> { stringProvider.getString(R.string.poll_end_room_list_preview) } else -> { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index c7be395693..a20c1e5f97 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -106,8 +106,8 @@ class NoticeEventFormatter @Inject constructor( EventType.STATE_SPACE_PARENT, EventType.REDACTION, EventType.STICKER, - EventType.POLL_RESPONSE, - EventType.POLL_END -> formatDebug(timelineEvent.root) + in EventType.POLL_RESPONSE, + in EventType.POLL_END -> formatDebug(timelineEvent.root) else -> { Timber.v("Type $type not handled by this formatter") null @@ -196,8 +196,8 @@ class NoticeEventFormatter @Inject constructor( } private fun formatDebug(event: Event): CharSequence { - val threadPrefix = if (event.isThread()) "thread" else "" - return "Debug: $threadPrefix event type \"${event.getClearType()}\"" + val threadPrefix = if (event.isThread()) "thread" else "" + return "Debug: $threadPrefix event type \"${event.getClearType()}\"" } private fun formatRoomCreateEvent(event: Event, isDm: Boolean): CharSequence? { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 53a9fbbaea..96a2ca4609 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -50,9 +50,8 @@ object TimelineDisplayableEvents { EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_JOIN_RULES, EventType.KEY_VERIFICATION_DONE, - EventType.KEY_VERIFICATION_CANCEL, - EventType.POLL_START - ) + EventType.KEY_VERIFICATION_CANCEL + ) + EventType.POLL_START } fun TimelineEvent.canBeMerged(): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt index c9fd1a1aa5..30c366738d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -106,8 +106,6 @@ abstract class AbsMessageItem : AbsBaseMessageItem holder.timeView.isVisible = false } - holder.additionalTopSpace.isVisible = attributes.informationData.messageLayout.addTopMargin - // Render send state indicator holder.sendStateImageView.render(attributes.informationData.sendStateDecoration) holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA @@ -157,7 +155,6 @@ abstract class AbsMessageItem : AbsBaseMessageItem abstract class Holder(@IdRes stubId: Int) : AbsBaseMessageItem.Holder(stubId) { - val additionalTopSpace by bind(R.id.additionalTopSpace) val avatarImageView by bind(R.id.messageAvatarImageView) val memberNameView by bind(R.id.messageMemberNameView) val timeView by bind(R.id.messageTimeView) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt index 202d8b05da..c0e668e013 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt @@ -21,43 +21,44 @@ import im.vector.app.R import kotlinx.parcelize.Parcelize sealed interface TimelineMessageLayout : Parcelable { + val layoutRes: Int val showAvatar: Boolean val showDisplayName: Boolean - val addTopMargin: Boolean val showTimestamp: Boolean @Parcelize - data class Default(override val showAvatar: Boolean, - override val showDisplayName: Boolean, - override val showTimestamp: Boolean, - override val addTopMargin: Boolean = false, - // Keep defaultLayout generated on epoxy items - override val layoutRes: Int = 0) : TimelineMessageLayout + data class Default( + override val showAvatar: Boolean, + override val showDisplayName: Boolean, + override val showTimestamp: Boolean, + // Keep defaultLayout generated on epoxy items + override val layoutRes: Int = 0, + ) : TimelineMessageLayout @Parcelize data class Bubble( - override val showAvatar: Boolean, - override val showDisplayName: Boolean, - override val showTimestamp: Boolean = true, - override val addTopMargin: Boolean = false, - val isIncoming: Boolean, - val isPseudoBubble: Boolean, - val cornersRadius: CornersRadius, - val timestampAsOverlay: Boolean, - override val layoutRes: Int = if (isIncoming) { - R.layout.item_timeline_event_bubble_incoming_base - } else { - R.layout.item_timeline_event_bubble_outgoing_base - } + override val showAvatar: Boolean, + override val showDisplayName: Boolean, + override val showTimestamp: Boolean = true, + val addTopMargin: Boolean = false, + val isIncoming: Boolean, + val isPseudoBubble: Boolean, + val cornersRadius: CornersRadius, + val timestampAsOverlay: Boolean, + override val layoutRes: Int = if (isIncoming) { + R.layout.item_timeline_event_bubble_incoming_base + } else { + R.layout.item_timeline_event_bubble_outgoing_base + }, ) : TimelineMessageLayout { @Parcelize data class CornersRadius( - val topStartRadius: Float, - val topEndRadius: Float, - val bottomStartRadius: Float, - val bottomEndRadius: Float + val topStartRadius: Float, + val topEndRadius: Float, + val bottomStartRadius: Float, + val bottomEndRadius: Float, ) : Parcelable } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index a614015f46..3e3e9775f8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -43,10 +43,9 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess // Can be rendered in bubbles, other types will fallback to default private val EVENT_TYPES_WITH_BUBBLE_LAYOUT = setOf( EventType.MESSAGE, - EventType.POLL_START, EventType.ENCRYPTED, EventType.STICKER - ) + ) + EventType.POLL_START // Can't be rendered in bubbles, so get back to default layout private val MSG_TYPES_WITHOUT_BUBBLE_LAYOUT = setOf( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index 93eae9a1d3..954aa0bf34 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -42,9 +42,11 @@ import im.vector.app.features.home.room.detail.timeline.style.shapeAppearanceMod import im.vector.app.features.themes.ThemeUtils import timber.log.Timber -class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = 0) : - RelativeLayout(context, attrs, defStyleAttr), TimelineMessageLayoutRenderer { +class MessageBubbleView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : RelativeLayout(context, attrs, defStyleAttr), TimelineMessageLayoutRenderer { private var isIncoming: Boolean = false @@ -87,22 +89,43 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri outlineProvider = ViewOutlineProvider.BACKGROUND clipToOutline = true background = RippleDrawable( - ContextCompat.getColorStateList(context, R.color.mtrl_btn_ripple_color) ?: ColorStateList.valueOf(Color.TRANSPARENT), - bubbleDrawable, - rippleMaskDrawable) + ContextCompat.getColorStateList(context, R.color.mtrl_btn_ripple_color) ?: ColorStateList.valueOf(Color.TRANSPARENT), + bubbleDrawable, + rippleMaskDrawable) } } override fun renderMessageLayout(messageLayout: TimelineMessageLayout) { - if (messageLayout !is TimelineMessageLayout.Bubble) { - Timber.v("Can't render messageLayout $messageLayout") - return + (messageLayout as? TimelineMessageLayout.Bubble) + ?.updateDrawables() + ?.setConstraintsAndColor() + ?.toggleMessageOverlay() + ?.setPadding() + ?.setMargins() + ?.setAdditionalTopSpace() + ?: Timber.v("Can't render messageLayout $messageLayout") + } + + private fun TimelineMessageLayout.Bubble.updateDrawables() = apply { + val shapeAppearanceModel = cornersRadius.shapeAppearanceModel() + bubbleDrawable.apply { + this.shapeAppearanceModel = shapeAppearanceModel + this.fillColor = if (isPseudoBubble) { + ColorStateList.valueOf(Color.TRANSPARENT) + } else { + val backgroundColorAttr = if (isIncoming) R.attr.vctr_message_bubble_inbound else R.attr.vctr_message_bubble_outbound + val backgroundColor = ThemeUtils.getColor(context, backgroundColorAttr) + ColorStateList.valueOf(backgroundColor) + } } - updateDrawables(messageLayout) + rippleMaskDrawable.shapeAppearanceModel = shapeAppearanceModel + } + + private fun TimelineMessageLayout.Bubble.setConstraintsAndColor() = apply { ConstraintSet().apply { clone(views.bubbleView) clear(R.id.viewStubContainer, ConstraintSet.END) - if (messageLayout.timestampAsOverlay) { + if (timestampAsOverlay) { val timeColor = ContextCompat.getColor(context, R.color.palette_white) views.messageTimeView.setTextColor(timeColor) connect(R.id.viewStubContainer, ConstraintSet.END, R.id.parent, ConstraintSet.END, 0) @@ -113,17 +136,26 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri } applyTo(views.bubbleView) } - if (messageLayout.timestampAsOverlay) { + } + + private fun TimelineMessageLayout.Bubble.toggleMessageOverlay() = apply { + if (timestampAsOverlay) { views.messageOverlayView.isVisible = true - (views.messageOverlayView.background as? GradientDrawable)?.cornerRadii = messageLayout.cornersRadius.toFloatArray() + (views.messageOverlayView.background as? GradientDrawable)?.cornerRadii = cornersRadius.toFloatArray() } else { views.messageOverlayView.isVisible = false } - if (messageLayout.isPseudoBubble && messageLayout.timestampAsOverlay) { + } + + private fun TimelineMessageLayout.Bubble.setPadding() = apply { + if (isPseudoBubble && timestampAsOverlay) { views.viewStubContainer.root.setPadding(0, 0, 0, 0) } else { views.viewStubContainer.root.setPadding(horizontalStubPadding, verticalStubPadding, horizontalStubPadding, verticalStubPadding) } + } + + private fun TimelineMessageLayout.Bubble.setMargins() = apply { if (isIncoming) { views.messageEndGuideline.updateLayoutParams { marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end) @@ -141,22 +173,11 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri } } + private fun TimelineMessageLayout.Bubble.setAdditionalTopSpace() = apply { + views.additionalTopSpace.isVisible = addTopMargin + } + private fun TimelineMessageLayout.Bubble.CornersRadius.toFloatArray(): FloatArray { return floatArrayOf(topStartRadius, topStartRadius, topEndRadius, topEndRadius, bottomEndRadius, bottomEndRadius, bottomStartRadius, bottomStartRadius) } - - private fun updateDrawables(messageLayout: TimelineMessageLayout.Bubble) { - val shapeAppearanceModel = messageLayout.cornersRadius.shapeAppearanceModel() - bubbleDrawable.apply { - this.shapeAppearanceModel = shapeAppearanceModel - this.fillColor = if (messageLayout.isPseudoBubble) { - ColorStateList.valueOf(Color.TRANSPARENT) - } else { - val backgroundColorAttr = if (isIncoming) R.attr.vctr_message_bubble_inbound else R.attr.vctr_message_bubble_outbound - val backgroundColor = ThemeUtils.getColor(context, backgroundColorAttr) - ColorStateList.valueOf(backgroundColor) - } - } - rippleMaskDrawable.shapeAppearanceModel = shapeAppearanceModel - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomCategoryItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomCategoryItem.kt index bec3ccc643..6057072e41 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomCategoryItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomCategoryItem.kt @@ -33,6 +33,7 @@ import im.vector.app.features.themes.ThemeUtils abstract class RoomCategoryItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var title: String + @EpoxyAttribute var itemCount: Int = 0 @EpoxyAttribute var expanded: Boolean = false @EpoxyAttribute var unreadNotificationCount: Int = 0 @EpoxyAttribute var showHighlighted: Boolean = false @@ -46,14 +47,16 @@ abstract class RoomCategoryItem : VectorEpoxyModel() { DrawableCompat.setTint(it, tintColor) } holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted)) - holder.titleView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null) holder.titleView.text = title + holder.counterView.text = itemCount.takeIf { it > 0 }?.toString().orEmpty() + holder.counterView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null) holder.rootView.onClick(listener) } class Holder : VectorEpoxyHolder() { val unreadCounterBadgeView by bind(R.id.roomCategoryUnreadCounterBadgeView) val titleView by bind(R.id.roomCategoryTitleView) + val counterView by bind(R.id.roomCategoryCounterView) val rootView by bind(R.id.roomCategoryRootView) } } 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 28849204c4..4265eebe62 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 @@ -23,6 +23,8 @@ import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager @@ -50,8 +52,10 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel import im.vector.app.features.home.room.list.widget.NotifsFabMenuView import im.vector.app.features.notifications.NotificationDrawerManager +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -287,6 +291,7 @@ class RoomListFragment @Inject constructor( )) checkEmptyState() } + observeItemCount(section, sectionAdapter) section.notificationCount.observe(viewLifecycleOwner) { counts -> sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( notificationCount = counts.totalCount, @@ -310,6 +315,7 @@ class RoomListFragment @Inject constructor( )) checkEmptyState() } + observeItemCount(section, sectionAdapter) section.isExpanded.observe(viewLifecycleOwner) { _ -> refreshCollapseStates() } @@ -326,6 +332,7 @@ class RoomListFragment @Inject constructor( isLoading = false)) checkEmptyState() } + observeItemCount(section, sectionAdapter) section.notificationCount.observe(viewLifecycleOwner) { counts -> sectionAdapter.updateSection(sectionAdapter.roomsSectionData.copy( notificationCount = counts.totalCount, @@ -373,6 +380,18 @@ class RoomListFragment @Inject constructor( } } + private fun observeItemCount(section: RoomsSection, sectionAdapter: SectionHeaderAdapter) { + lifecycleScope.launch { + section.itemCount + .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) + .collect { count -> + sectionAdapter.updateSection( + sectionAdapter.roomsSectionData.copy(itemCount = count) + ) + } + } + } + private fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) { when (quickAction) { is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt index 77f61149f8..ec7915ba34 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt @@ -28,6 +28,7 @@ import im.vector.app.features.invite.showInvites import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -72,7 +73,18 @@ class RoomListSectionBuilderGroup( session.getFilteredPagedRoomSummariesLive(qpm) .let { updatableFilterLivePageResult -> onUpdatable(updatableFilterLivePageResult) - sections.add(RoomsSection(name, updatableFilterLivePageResult.livePagedList)) + + val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow() + .flatMapLatest { session.getRoomCountFlow(updatableFilterLivePageResult.queryParams) } + .distinctUntilChanged() + + sections.add( + RoomsSection( + sectionName = name, + livePages = updatableFilterLivePageResult.livePagedList, + itemCount = itemCountFlow + ) + ) } } ) @@ -109,9 +121,7 @@ class RoomListSectionBuilderGroup( .onEach { groupingMethod -> val selectedGroupId = (groupingMethod.orNull() as? RoomGroupingMethod.ByLegacyGroup)?.groupSummary?.groupId activeGroupAwareQueries.onEach { updater -> - updater.updateQuery { query -> - query.copy(activeGroupId = selectedGroupId) - } + updater.queryParams = updater.queryParams.copy(activeGroupId = selectedGroupId) } }.launchIn(coroutineScope) @@ -265,7 +275,8 @@ class RoomListSectionBuilderGroup( RoomsSection( sectionName = name, livePages = livePagedList, - notifyOfLocalEcho = notifyOfLocalEcho + notifyOfLocalEcho = notifyOfLocalEcho, + itemCount = session.getRoomCountFlow(roomQueryParams) ) ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt index 296e61690b..f82dbd43e1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.ActiveSpaceFilter @@ -91,7 +92,18 @@ class RoomListSectionBuilderSpace( session.getFilteredPagedRoomSummariesLive(qpm) .let { updatableFilterLivePageResult -> onUpdatable(updatableFilterLivePageResult) - sections.add(RoomsSection(name, updatableFilterLivePageResult.livePagedList)) + + val itemCountFlow = updatableFilterLivePageResult.livePagedList.asFlow() + .flatMapLatest { session.getRoomCountFlow(updatableFilterLivePageResult.queryParams) } + .distinctUntilChanged() + + sections.add( + RoomsSection( + sectionName = name, + livePages = updatableFilterLivePageResult.livePagedList, + itemCount = itemCountFlow + ) + ) } } ) @@ -261,7 +273,8 @@ class RoomListSectionBuilderSpace( RoomsSection( sectionName = stringProvider.getString(R.string.suggested_header), liveSuggested = liveSuggestedRooms, - notifyOfLocalEcho = false + notifyOfLocalEcho = false, + itemCount = suggestedRoomsFlow.map { suggestions -> suggestions.size } ) ) } @@ -338,11 +351,9 @@ class RoomListSectionBuilderSpace( RoomListViewModel.SpaceFilterStrategy.ORPHANS_IF_SPACE_NULL -> { activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater { override fun updateForSpaceId(roomId: String?) { - it.updateQuery { - it.copy( - activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId) - ) - } + it.queryParams = roomQueryParams.copy( + activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId) + ) } }) } @@ -350,17 +361,13 @@ class RoomListSectionBuilderSpace( activeSpaceUpdaters.add(object : RoomListViewModel.ActiveSpaceQueryUpdater { override fun updateForSpaceId(roomId: String?) { if (roomId != null) { - it.updateQuery { - it.copy( - activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId) - ) - } + it.queryParams = roomQueryParams.copy( + activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(roomId) + ) } else { - it.updateQuery { - it.copy( - activeSpaceFilter = ActiveSpaceFilter.None - ) - } + it.queryParams = roomQueryParams.copy( + activeSpaceFilter = ActiveSpaceFilter.None + ) } } }) @@ -390,11 +397,19 @@ class RoomListSectionBuilderSpace( .flowOn(Dispatchers.Default) .launchIn(viewModelScope) + val itemCountFlow = livePagedList.asFlow() + .flatMapLatest { + val queryParams = roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId()) + session.getRoomCountFlow(queryParams) + } + .distinctUntilChanged() + sections.add( RoomsSection( sectionName = name, livePages = livePagedList, - notifyOfLocalEcho = notifyOfLocalEcho + notifyOfLocalEcho = notifyOfLocalEcho, + itemCount = itemCountFlow ) ) } 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 4a81a8b526..ec8b01876b 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 @@ -192,8 +192,8 @@ class RoomListViewModel @AssistedInject constructor( roomFilter = action.filter ) } - updatableQuery?.updateQuery { - it.copy( + updatableQuery?.apply { + queryParams = queryParams.copy( displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.NORMALIZED) ) } 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 index 5eaae262a6..357df5ecd3 100644 --- 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 @@ -19,6 +19,7 @@ package im.vector.app.features.home.room.list import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.paging.PagedList +import kotlinx.coroutines.flow.Flow import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount @@ -29,6 +30,7 @@ data class RoomsSection( val liveList: LiveData>? = null, val liveSuggested: LiveData? = null, val isExpanded: MutableLiveData = MutableLiveData(true), + val itemCount: Flow, 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 index 560e0d00a3..2e6436d21d 100644 --- 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 @@ -33,6 +33,7 @@ class SectionHeaderAdapter constructor( data class RoomsSectionData( val name: String, + val itemCount: Int = 0, val isExpanded: Boolean = true, val notificationCount: Int = 0, val isHighlighted: Boolean = false, @@ -85,8 +86,9 @@ class SectionHeaderAdapter constructor( val expandedArrowDrawable = ContextCompat.getDrawable(binding.root.context, expandedArrowDrawableRes)?.also { DrawableCompat.setTint(it, tintColor) } + binding.roomCategoryCounterView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null) + binding.roomCategoryCounterView.text = roomsSectionData.itemCount.takeIf { it > 0 }?.toString().orEmpty() binding.roomCategoryUnreadCounterBadgeView.render(UnreadCounterBadgeView.State(roomsSectionData.notificationCount, roomsSectionData.isHighlighted)) - binding.roomCategoryTitleView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null) } companion object { diff --git a/vector/src/main/java/im/vector/app/features/location/UrlMapProvider.kt b/vector/src/main/java/im/vector/app/features/location/UrlMapProvider.kt index adb5c27a02..3e4e16861e 100644 --- a/vector/src/main/java/im/vector/app/features/location/UrlMapProvider.kt +++ b/vector/src/main/java/im/vector/app/features/location/UrlMapProvider.kt @@ -39,7 +39,7 @@ class UrlMapProvider @Inject constructor( suspend fun getMapUrl(): String { val upstreamMapUrl = tryOrNull { rawService.getElementWellknown(session.sessionParams) } - ?.mapTileServerConfig + ?.getBestMapTileServerConfig() ?.mapStyleUrl return upstreamMapUrl ?: fallbackMapUrl } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt index b35c110892..4f16231747 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingAction.kt @@ -75,6 +75,7 @@ sealed class OnboardingAction : VectorViewModelAction { data class UserAcceptCertificate(val fingerprint: Fingerprint) : OnboardingAction() + object PersonalizeProfile : OnboardingAction() data class UpdateDisplayName(val displayName: String) : OnboardingAction() object UpdateDisplayNameSkipped : OnboardingAction() data class ProfilePictureSelected(val uri: Uri) : OnboardingAction() diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt index 8a09879b15..82ee48411d 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewEvents.kt @@ -51,9 +51,8 @@ sealed class OnboardingViewEvents : VectorViewEvents { object OnAccountCreated : OnboardingViewEvents() object OnAccountSignedIn : OnboardingViewEvents() object OnTakeMeHome : OnboardingViewEvents() - object OnPersonalizeProfile : OnboardingViewEvents() - object OnDisplayNameUpdated : OnboardingViewEvents() - object OnDisplayNameSkipped : OnboardingViewEvents() + object OnChooseDisplayName : OnboardingViewEvents() + object OnChooseProfilePicture : OnboardingViewEvents() object OnPersonalizationComplete : OnboardingViewEvents() object OnBack : OnboardingViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt index 413745f98c..36020fbe61 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt @@ -48,6 +48,7 @@ import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.login.ServerType import im.vector.app.features.login.SignMode import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixPatterns.getDomain import org.matrix.android.sdk.api.auth.AuthenticationService @@ -156,12 +157,13 @@ class OnboardingViewModel @AssistedInject constructor( is OnboardingAction.ResetAction -> handleResetAction(action) is OnboardingAction.UserAcceptCertificate -> handleUserAcceptCertificate(action) OnboardingAction.ClearHomeServerHistory -> handleClearHomeServerHistory() - is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent) is OnboardingAction.UpdateDisplayName -> updateDisplayName(action.displayName) - OnboardingAction.UpdateDisplayNameSkipped -> _viewEvents.post(OnboardingViewEvents.OnDisplayNameSkipped) - OnboardingAction.UpdateProfilePictureSkipped -> _viewEvents.post(OnboardingViewEvents.OnPersonalizationComplete) + OnboardingAction.UpdateDisplayNameSkipped -> handleDisplayNameStepComplete() + OnboardingAction.UpdateProfilePictureSkipped -> completePersonalization() + OnboardingAction.PersonalizeProfile -> handlePersonalizeProfile() is OnboardingAction.ProfilePictureSelected -> handleProfilePictureSelected(action) OnboardingAction.SaveSelectedProfilePicture -> updateProfilePicture() + is OnboardingAction.PostViewEvent -> _viewEvents.post(action.viewEvent) }.exhaustive } @@ -762,15 +764,33 @@ class OnboardingViewModel @AssistedInject constructor( authenticationService.reset() session.configureAndStart(applicationContext) - setState { - copy( - asyncLoginAction = Success(Unit) - ) - } when (isAccountCreated) { - true -> _viewEvents.post(OnboardingViewEvents.OnAccountCreated) - false -> _viewEvents.post(OnboardingViewEvents.OnAccountSignedIn) + true -> { + val personalizationState = createPersonalizationState(session, state) + setState { + copy(asyncLoginAction = Success(Unit), personalizationState = personalizationState) + } + _viewEvents.post(OnboardingViewEvents.OnAccountCreated) + } + false -> { + setState { copy(asyncLoginAction = Success(Unit)) } + _viewEvents.post(OnboardingViewEvents.OnAccountSignedIn) + } + } + } + + private suspend fun createPersonalizationState(session: Session, state: OnboardingViewState): PersonalizationState { + return when { + vectorFeatures.isOnboardingPersonalizeEnabled() -> { + val homeServerCapabilities = session.getHomeServerCapabilities() + val capabilityOverrides = vectorOverrides.forceHomeserverCapabilities?.firstOrNull() + state.personalizationState.copy( + supportsChangingDisplayName = capabilityOverrides?.canChangeDisplayName ?: homeServerCapabilities.canChangeDisplayName, + supportsChangingProfilePicture = capabilityOverrides?.canChangeAvatar ?: homeServerCapabilities.canChangeAvatar + ) + } + else -> state.personalizationState } } @@ -910,7 +930,7 @@ class OnboardingViewModel @AssistedInject constructor( personalizationState = personalizationState.copy(displayName = displayName) ) } - _viewEvents.post(OnboardingViewEvents.OnDisplayNameUpdated) + handleDisplayNameStepComplete() } catch (error: Throwable) { setState { copy(asyncDisplayName = Fail(error)) } _viewEvents.post(OnboardingViewEvents.Failure(error)) @@ -918,12 +938,37 @@ class OnboardingViewModel @AssistedInject constructor( } } + private fun handlePersonalizeProfile() { + withPersonalisationState { + when { + it.supportsChangingDisplayName -> _viewEvents.post(OnboardingViewEvents.OnChooseDisplayName) + it.supportsChangingProfilePicture -> _viewEvents.post(OnboardingViewEvents.OnChooseProfilePicture) + else -> { + throw IllegalStateException("It should not be possible to personalize without supporting display name or avatar changing") + } + } + } + } + + private fun handleDisplayNameStepComplete() { + withPersonalisationState { + when { + it.supportsChangingProfilePicture -> _viewEvents.post(OnboardingViewEvents.OnChooseProfilePicture) + else -> completePersonalization() + } + } + } + private fun handleProfilePictureSelected(action: OnboardingAction.ProfilePictureSelected) { setState { copy(personalizationState = personalizationState.copy(selectedPictureUri = action.uri)) } } + private fun withPersonalisationState(block: (PersonalizationState) -> Unit) { + withState { block(it.personalizationState) } + } + private fun updateProfilePicture() { withState { state -> when (val pictureUri = state.personalizationState.selectedPictureUri) { @@ -955,6 +1000,10 @@ class OnboardingViewModel @AssistedInject constructor( } private fun onProfilePictureSaved() { + completePersonalization() + } + + private fun completePersonalization() { _viewEvents.post(OnboardingViewEvents.OnPersonalizationComplete) } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt index bd5d93ae4d..8747de6da8 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewState.kt @@ -22,7 +22,6 @@ import com.airbnb.mvrx.Async import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.PersistState -import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import im.vector.app.features.login.LoginMode import im.vector.app.features.login.ServerType @@ -83,10 +82,6 @@ data class OnboardingViewState( asyncDisplayName is Loading || asyncProfilePicture is Loading } - - fun isAuthTaskCompleted(): Boolean { - return asyncLoginAction is Success - } } enum class OnboardingFlow { @@ -97,6 +92,11 @@ enum class OnboardingFlow { @Parcelize data class PersonalizationState( + val supportsChangingDisplayName: Boolean = false, + val supportsChangingProfilePicture: Boolean = false, val displayName: String? = null, val selectedPictureUri: Uri? = null -) : Parcelable +) : Parcelable { + + fun supportsPersonalization() = supportsChangingDisplayName || supportsChangingProfilePicture +} diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt index d021fd2813..ccfb863a5b 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt @@ -20,11 +20,13 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.databinding.FragmentFtueAccountCreatedBinding import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingViewEvents +import im.vector.app.features.onboarding.OnboardingViewState import javax.inject.Inject class FtueAuthAccountCreatedFragment @Inject constructor( @@ -42,8 +44,15 @@ class FtueAuthAccountCreatedFragment @Inject constructor( private fun setupViews() { views.accountCreatedSubtitle.text = getString(R.string.ftue_account_created_subtitle, activeSessionHolder.getActiveSession().myUserId) - views.accountCreatedPersonalize.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnPersonalizeProfile)) } + views.accountCreatedPersonalize.debouncedClicks { viewModel.handle(OnboardingAction.PersonalizeProfile) } views.accountCreatedTakeMeHome.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) } + views.accountCreatedTakeMeHomeCta.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) } + } + + override fun updateWithState(state: OnboardingViewState) { + val canPersonalize = state.personalizationState.supportsPersonalization() + views.personalizeButtonGroup.isVisible = canPersonalize + views.takeMeHomeButtonGroup.isVisible = !canPersonalize } override fun resetViewModel() { diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseProfilePictureFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseProfilePictureFragment.kt index bc1bf0c8bc..81300932db 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseProfilePictureFragment.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthChooseProfilePictureFragment.kt @@ -22,6 +22,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.core.view.isInvisible import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder @@ -70,6 +71,8 @@ class FtueAuthChooseProfilePictureFragment @Inject constructor( } override fun updateWithState(state: OnboardingViewState) { + views.profilePictureToolbar.isInvisible = !state.personalizationState.supportsChangingDisplayName + val hasSetPicture = state.personalizationState.selectedPictureUri != null views.profilePictureSubmit.isEnabled = hasSetPicture views.changeProfilePictureIcon.setImageResource(if (hasSetPicture) R.drawable.ic_edit else R.drawable.ic_camera_plain) @@ -93,4 +96,14 @@ class FtueAuthChooseProfilePictureFragment @Inject constructor( override fun resetViewModel() { // Nothing to do } + + override fun onBackPressed(toolbarButton: Boolean): Boolean { + return when (withState(viewModel) { it.personalizationState.supportsChangingDisplayName }) { + true -> super.onBackPressed(toolbarButton) + false -> { + viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) + true + } + } + } } diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt index e336419e3f..2008726ac3 100644 --- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt +++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt @@ -122,17 +122,9 @@ class FtueAuthVariant( private fun updateWithState(viewState: OnboardingViewState) { isForceLoginFallbackEnabled = viewState.isForceLoginFallbackEnabled - views.loginLoading.isVisible = shouldShowLoading(viewState) + views.loginLoading.isVisible = viewState.isLoading() } - private fun shouldShowLoading(viewState: OnboardingViewState) = - if (vectorFeatures.isOnboardingPersonalizeEnabled()) { - viewState.isLoading() - } else { - // Keep loading when during success because of the delay when switching to the next Activity - viewState.isLoading() || viewState.isAuthTaskCompleted() - } - override fun setIsLoading(isLoading: Boolean) = Unit private fun handleOnboardingViewEvents(viewEvents: OnboardingViewEvents) { @@ -230,12 +222,11 @@ class FtueAuthVariant( FtueAuthUseCaseFragment::class.java, option = commonOption) } - OnboardingViewEvents.OnAccountCreated -> onAccountCreated() + is OnboardingViewEvents.OnAccountCreated -> onAccountCreated() OnboardingViewEvents.OnAccountSignedIn -> onAccountSignedIn() - OnboardingViewEvents.OnPersonalizeProfile -> onPersonalizeProfile() + OnboardingViewEvents.OnChooseDisplayName -> onChooseDisplayName() OnboardingViewEvents.OnTakeMeHome -> navigateToHome(createdAccount = true) - OnboardingViewEvents.OnDisplayNameUpdated -> onDisplayNameUpdated() - OnboardingViewEvents.OnDisplayNameSkipped -> onDisplayNameUpdated() + OnboardingViewEvents.OnChooseProfilePicture -> onChooseProfilePicture() OnboardingViewEvents.OnPersonalizationComplete -> navigateToHome(createdAccount = true) OnboardingViewEvents.OnBack -> activity.popBackstack() }.exhaustive @@ -399,15 +390,11 @@ class FtueAuthVariant( } private fun onAccountCreated() { - if (vectorFeatures.isOnboardingPersonalizeEnabled()) { - activity.supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) - activity.replaceFragment( - views.loginFragmentContainer, - FtueAuthAccountCreatedFragment::class.java, - ) - } else { - navigateToHome(createdAccount = true) - } + activity.supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + activity.replaceFragment( + views.loginFragmentContainer, + FtueAuthAccountCreatedFragment::class.java + ) } private fun navigateToHome(createdAccount: Boolean) { @@ -416,14 +403,14 @@ class FtueAuthVariant( activity.finish() } - private fun onPersonalizeProfile() { + private fun onChooseDisplayName() { activity.addFragmentToBackstack(views.loginFragmentContainer, FtueAuthChooseDisplayNameFragment::class.java, option = commonOption ) } - private fun onDisplayNameUpdated() { + private fun onChooseProfilePicture() { activity.addFragmentToBackstack(views.loginFragmentContainer, FtueAuthChooseProfilePictureFragment::class.java, option = commonOption diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt index b4f61dbc1f..0ef92e4d2f 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollController.kt @@ -60,9 +60,9 @@ class CreatePollController @Inject constructor( pollTypeChangedListener { _, id -> host.callback?.onPollTypeChanged( if (id == R.id.openPollTypeRadioButton) { - PollType.DISCLOSED + PollType.DISCLOSED_UNSTABLE } else { - PollType.UNDISCLOSED + PollType.UNDISCLOSED_UNSTABLE } ) } diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt index 5c7ef72297..2358f7f9a0 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewModel.kt @@ -71,9 +71,10 @@ class CreatePollViewModel @AssistedInject constructor( val event = room.getTimelineEvent(eventId) ?: return val content = event.getLastMessageContent() as? MessagePollContent ?: return - val pollType = content.pollCreationInfo?.kind ?: PollType.DISCLOSED - val question = content.pollCreationInfo?.question?.question ?: "" - val options = content.pollCreationInfo?.answers?.mapNotNull { it.answer } ?: List(MIN_OPTIONS_COUNT) { "" } + val pollCreationInfo = content.getBestPollCreationInfo() + val pollType = pollCreationInfo?.kind ?: PollType.DISCLOSED_UNSTABLE + val question = pollCreationInfo?.question?.getBestQuestion() ?: "" + val options = pollCreationInfo?.answers?.mapNotNull { it.getBestAnswer() } ?: List(MIN_OPTIONS_COUNT) { "" } setState { copy( diff --git a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt index 175d1b0116..fc3b746f32 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/CreatePollViewState.kt @@ -27,7 +27,7 @@ data class CreatePollViewState( val options: List = List(CreatePollViewModel.MIN_OPTIONS_COUNT) { "" }, val canCreatePoll: Boolean = false, val canAddMoreOptions: Boolean = true, - val pollType: PollType = PollType.DISCLOSED + val pollType: PollType = PollType.DISCLOSED_UNSTABLE ) : MavericksState { constructor(args: CreatePollArgs) : this( diff --git a/vector/src/main/java/im/vector/app/features/poll/create/PollTypeSelectionItem.kt b/vector/src/main/java/im/vector/app/features/poll/create/PollTypeSelectionItem.kt index 1b24a70cb9..0736c236b5 100644 --- a/vector/src/main/java/im/vector/app/features/poll/create/PollTypeSelectionItem.kt +++ b/vector/src/main/java/im/vector/app/features/poll/create/PollTypeSelectionItem.kt @@ -28,7 +28,7 @@ import org.matrix.android.sdk.api.session.room.model.message.PollType abstract class PollTypeSelectionItem : VectorEpoxyModel() { @EpoxyAttribute - var pollType: PollType = PollType.DISCLOSED + var pollType: PollType = PollType.DISCLOSED_UNSTABLE @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var pollTypeChangedListener: RadioGroup.OnCheckedChangeListener? = null @@ -38,8 +38,8 @@ abstract class PollTypeSelectionItem : VectorEpoxyModel R.id.openPollTypeRadioButton - PollType.UNDISCLOSED -> R.id.closedPollTypeRadioButton + PollType.DISCLOSED_UNSTABLE, PollType.DISCLOSED -> R.id.openPollTypeRadioButton + PollType.UNDISCLOSED_UNSTABLE, PollType.UNDISCLOSED -> R.id.closedPollTypeRadioButton } ) diff --git a/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnown.kt b/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnown.kt index 0ae2a16b71..91b0f4d2f7 100644 --- a/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnown.kt +++ b/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnown.kt @@ -38,8 +38,13 @@ data class ElementWellKnown( val riotE2E: E2EWellKnownConfig? = null, @Json(name = "org.matrix.msc3488.tile_server") + val unstableMapTileServerConfig: MapTileServerConfig? = null, + + @Json(name = "m.tile_server") val mapTileServerConfig: MapTileServerConfig? = null -) +) { + fun getBestMapTileServerConfig() = mapTileServerConfig ?: unstableMapTileServerConfig +} @JsonClass(generateAdapter = true) data class E2EWellKnownConfig( diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt index 3164daf634..c2d63aa8d3 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt @@ -94,6 +94,12 @@ class AddRoomListController @Inject constructor( } var totalSize: Int = 0 + set(value) { + if (value != field) { + field = value + requestForcedModelBuild() + } + } var selectedItems: Map = emptyMap() set(value) { @@ -120,7 +126,8 @@ class AddRoomListController @Inject constructor( add( RoomCategoryItem_().apply { id("header") - title(host.sectionName ?: "") + title(host.sectionName.orEmpty()) + itemCount(host.totalSize) expanded(host.expanded) listener { host.expanded = !host.expanded diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt index bcf0a8a949..8d6a351013 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomFragment.kt @@ -22,6 +22,8 @@ import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager @@ -35,9 +37,12 @@ import im.vector.app.core.extensions.cleanup import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentSpaceAddRoomsBinding +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.room.model.RoomSummary import reactivecircus.flowbinding.appcompat.queryTextChanges import javax.inject.Inject @@ -169,48 +174,63 @@ class SpaceAddRoomFragment @Inject constructor( } private fun setupRecyclerView() { - val concatAdapter = ConcatAdapter() - spaceEpoxyController.sectionName = getString(R.string.spaces_header) - roomEpoxyController.sectionName = getString(R.string.rooms_header) - spaceEpoxyController.listener = this - roomEpoxyController.listener = this + setupSpaceSection() + setupRoomSection() + setupDmSection() - viewModel.updatableLiveSpacePageResult.liveBoundaries.observe(viewLifecycleOwner) { + views.roomList.adapter = ConcatAdapter().apply { + addAdapter(roomEpoxyController.adapter) + addAdapter(spaceEpoxyController.adapter) + addAdapter(dmEpoxyController.adapter) + } + } + + private fun setupSpaceSection() { + spaceEpoxyController.sectionName = getString(R.string.spaces_header) + spaceEpoxyController.listener = this + viewModel.spaceUpdatableLivePageResult.liveBoundaries.observe(viewLifecycleOwner) { spaceEpoxyController.boundaryChange(it) } - viewModel.updatableLiveSpacePageResult.livePagedList.observe(viewLifecycleOwner) { - spaceEpoxyController.totalSize = it.size + viewModel.spaceUpdatableLivePageResult.livePagedList.observe(viewLifecycleOwner) { spaceEpoxyController.submitList(it) } + listenItemCount(viewModel.spaceCountFlow) { spaceEpoxyController.totalSize = it } + } - viewModel.updatableLivePageResult.liveBoundaries.observe(viewLifecycleOwner) { + private fun setupRoomSection() { + roomEpoxyController.sectionName = getString(R.string.rooms_header) + roomEpoxyController.listener = this + + viewModel.roomUpdatableLivePageResult.liveBoundaries.observe(viewLifecycleOwner) { roomEpoxyController.boundaryChange(it) } - viewModel.updatableLivePageResult.livePagedList.observe(viewLifecycleOwner) { - roomEpoxyController.totalSize = it.size + viewModel.roomUpdatableLivePageResult.livePagedList.observe(viewLifecycleOwner) { roomEpoxyController.submitList(it) } - + listenItemCount(viewModel.roomCountFlow) { roomEpoxyController.totalSize = it } views.roomList.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) views.roomList.setHasFixedSize(true) + } - concatAdapter.addAdapter(roomEpoxyController.adapter) - concatAdapter.addAdapter(spaceEpoxyController.adapter) - + private fun setupDmSection() { // This controller can be disabled depending on the space type (public or not) - viewModel.updatableDMLivePageResult.liveBoundaries.observe(viewLifecycleOwner) { - dmEpoxyController.boundaryChange(it) - } - viewModel.updatableDMLivePageResult.livePagedList.observe(viewLifecycleOwner) { - dmEpoxyController.totalSize = it.size - dmEpoxyController.submitList(it) - } dmEpoxyController.sectionName = getString(R.string.direct_chats_header) dmEpoxyController.listener = this + viewModel.dmUpdatableLivePageResult.liveBoundaries.observe(viewLifecycleOwner) { + dmEpoxyController.boundaryChange(it) + } + viewModel.dmUpdatableLivePageResult.livePagedList.observe(viewLifecycleOwner) { + dmEpoxyController.submitList(it) + } + listenItemCount(viewModel.dmCountFlow) { dmEpoxyController.totalSize = it } + } - concatAdapter.addAdapter(dmEpoxyController.adapter) - - views.roomList.adapter = concatAdapter + private fun listenItemCount(itemCountFlow: Flow, onEachAction: (Int) -> Unit) { + lifecycleScope.launch { + itemCountFlow + .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) + .collect { count -> onEachAction(count) } + } } override fun onBackPressed(toolbarButton: Boolean): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt index 8fa269d439..7d99c53f23 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt @@ -17,7 +17,7 @@ package im.vector.app.features.spaces.manage import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.asFlow import androidx.paging.PagedList import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading @@ -30,6 +30,9 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.query.ActiveSpaceFilter @@ -60,7 +63,7 @@ class SpaceAddRoomsViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() - val updatableLiveSpacePageResult: UpdatableLivePageResult by lazy { + val spaceUpdatableLivePageResult: UpdatableLivePageResult by lazy { session.getFilteredPagedRoomSummariesLive( roomSummaryQueryParams { this.memberships = listOf(Membership.JOIN) @@ -79,7 +82,13 @@ class SpaceAddRoomsViewModel @AssistedInject constructor( ) } - val updatableLivePageResult: UpdatableLivePageResult by lazy { + val spaceCountFlow: Flow by lazy { + spaceUpdatableLivePageResult.livePagedList.asFlow() + .flatMapLatest { session.getRoomCountFlow(spaceUpdatableLivePageResult.queryParams) } + .distinctUntilChanged() + } + + val roomUpdatableLivePageResult: UpdatableLivePageResult by lazy { session.getFilteredPagedRoomSummariesLive( roomSummaryQueryParams { this.memberships = listOf(Membership.JOIN) @@ -99,7 +108,13 @@ class SpaceAddRoomsViewModel @AssistedInject constructor( ) } - val updatableDMLivePageResult: UpdatableLivePageResult by lazy { + val roomCountFlow: Flow by lazy { + roomUpdatableLivePageResult.livePagedList.asFlow() + .flatMapLatest { session.getRoomCountFlow(roomUpdatableLivePageResult.queryParams) } + .distinctUntilChanged() + } + + val dmUpdatableLivePageResult: UpdatableLivePageResult by lazy { session.getFilteredPagedRoomSummariesLive( roomSummaryQueryParams { this.memberships = listOf(Membership.JOIN) @@ -119,6 +134,12 @@ class SpaceAddRoomsViewModel @AssistedInject constructor( ) } + val dmCountFlow: Flow by lazy { + dmUpdatableLivePageResult.livePagedList.asFlow() + .flatMapLatest { session.getRoomCountFlow(dmUpdatableLivePageResult.queryParams) } + .distinctUntilChanged() + } + private val selectionList = mutableMapOf() val selectionListLiveData = MutableLiveData>() @@ -143,17 +164,13 @@ class SpaceAddRoomsViewModel @AssistedInject constructor( override fun handle(action: SpaceAddRoomActions) { when (action) { - is SpaceAddRoomActions.UpdateFilter -> { - updatableLivePageResult.updateQuery { - it.copy( - displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.INSENSITIVE) - ) - } - updatableLiveSpacePageResult.updateQuery { - it.copy( - displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.INSENSITIVE) - ) - } + is SpaceAddRoomActions.UpdateFilter -> { + roomUpdatableLivePageResult.queryParams = roomUpdatableLivePageResult.queryParams.copy( + displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.INSENSITIVE) + ) + roomUpdatableLivePageResult.queryParams = roomUpdatableLivePageResult.queryParams.copy( + displayName = QueryStringValue.Contains(action.filter, QueryStringValue.Case.INSENSITIVE) + ) setState { copy( currentFilter = action.filter @@ -164,7 +181,7 @@ class SpaceAddRoomsViewModel @AssistedInject constructor( selectionList[action.roomSummary.roomId] = (selectionList[action.roomSummary.roomId] ?: false).not() selectionListLiveData.postValue(selectionList.toMap()) } - SpaceAddRoomActions.Save -> { + SpaceAddRoomActions.Save -> { doAddSelectedRooms() } } diff --git a/vector/src/main/res/layout/fragment_ftue_account_created.xml b/vector/src/main/res/layout/fragment_ftue_account_created.xml index 1985af1d5e..65bcdf2b63 100644 --- a/vector/src/main/res/layout/fragment_ftue_account_created.xml +++ b/vector/src/main/res/layout/fragment_ftue_account_created.xml @@ -86,6 +86,14 @@ app:layout_constraintBottom_toTopOf="@id/accountCreatedPersonalize" app:layout_constraintTop_toBottomOf="@id/accountCreatedSubtitle" /> + +