From 6e57b06673adaac2ade9b41cbb37b4777a9efbf9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 May 2020 22:38:07 +0200 Subject: [PATCH 01/17] Ensure Filter model match the spec and add Javadoc --- .../database/query/FilterEntityQueries.kt | 2 +- .../session/filter/DefaultFilterRepository.kt | 20 +++---- .../session/filter/DefaultSaveFilterTask.kt | 4 +- .../internal/session/filter/EventFilter.kt | 59 +++++++++++++++++++ .../android/internal/session/filter/Filter.kt | 48 +++++++++------ .../internal/session/filter/FilterApi.kt | 5 +- .../internal/session/filter/FilterBody.kt | 39 ------------ .../internal/session/filter/FilterFactory.kt | 8 +-- .../session/filter/FilterRepository.kt | 4 +- .../internal/session/filter/FilterResponse.kt | 5 ++ .../internal/session/filter/FilterUtil.kt | 20 +++---- .../session/filter/RoomEventFilter.kt | 32 ++++++++++ .../internal/session/filter/RoomFilter.kt | 25 ++++++++ 13 files changed, 183 insertions(+), 88 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/EventFilter.kt delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterBody.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt index 6902d39a82..42a84113ca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterEntityQueries.kt @@ -35,7 +35,7 @@ internal fun FilterEntity.Companion.get(realm: Realm): FilterEntity? { internal fun FilterEntity.Companion.getOrCreate(realm: Realm): FilterEntity { return get(realm) ?: realm.createObject() .apply { - filterBodyJson = FilterFactory.createDefaultFilterBody().toJSONString() + filterBodyJson = FilterFactory.createDefaultFilter().toJSONString() roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString() filterId = "" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt index f7df8c512e..95291de4b6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultFilterRepository.kt @@ -28,25 +28,25 @@ import javax.inject.Inject internal class DefaultFilterRepository @Inject constructor(private val monarchy: Monarchy) : FilterRepository { - override suspend fun storeFilter(filterBody: FilterBody, roomEventFilter: RoomEventFilter): Boolean { + override suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> - val filter = FilterEntity.get(realm) + val filterEntity = FilterEntity.get(realm) // Filter has changed, or no filter Id yet - filter == null - || filter.filterBodyJson != filterBody.toJSONString() - || filter.filterId.isBlank() + filterEntity == null + || filterEntity.filterBodyJson != filter.toJSONString() + || filterEntity.filterId.isBlank() }.also { hasChanged -> if (hasChanged) { // Filter is new or has changed, store it and reset the filter Id. // This has to be done outside of the Realm.use(), because awaitTransaction change the current thread monarchy.awaitTransaction { realm -> // We manage only one filter for now - val filterBodyJson = filterBody.toJSONString() + val filterJson = filter.toJSONString() val roomEventFilterJson = roomEventFilter.toJSONString() val filterEntity = FilterEntity.getOrCreate(realm) - filterEntity.filterBodyJson = filterBodyJson + filterEntity.filterBodyJson = filterJson filterEntity.roomEventFilterJson = roomEventFilterJson // Reset filterId filterEntity.filterId = "" @@ -55,14 +55,14 @@ internal class DefaultFilterRepository @Inject constructor(private val monarchy: } } - override suspend fun storeFilterId(filterBody: FilterBody, filterId: String) { + override suspend fun storeFilterId(filter: Filter, filterId: String) { monarchy.awaitTransaction { // We manage only one filter for now - val filterBodyJson = filterBody.toJSONString() + val filterJson = filter.toJSONString() // Update the filter id, only if the filter body matches it.where() - .equalTo(FilterEntityFields.FILTER_BODY_JSON, filterBodyJson) + .equalTo(FilterEntityFields.FILTER_BODY_JSON, filterJson) ?.findFirst() ?.filterId = filterId } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultSaveFilterTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultSaveFilterTask.kt index 47c5e4a08a..f396e01e86 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultSaveFilterTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/DefaultSaveFilterTask.kt @@ -43,10 +43,10 @@ internal class DefaultSaveFilterTask @Inject constructor( override suspend fun execute(params: SaveFilterTask.Params) { val filterBody = when (params.filterPreset) { FilterService.FilterPreset.RiotFilter -> { - FilterFactory.createRiotFilterBody() + FilterFactory.createRiotFilter() } FilterService.FilterPreset.NoFilter -> { - FilterFactory.createDefaultFilterBody() + FilterFactory.createDefaultFilter() } } val roomFilter = when (params.filterPreset) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/EventFilter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/EventFilter.kt new file mode 100644 index 0000000000..f5d159588b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/EventFilter.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.session.filter + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Represents "Filter" as mentioned in the SPEC + * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter + */ +@JsonClass(generateAdapter = true) +data class EventFilter( + /** + * The maximum number of events to return. + */ + @Json(name = "limit") val limit: Int? = null, + /** + * A list of senders IDs to include. If this list is absent then all senders are included. + */ + @Json(name = "senders") val senders: List? = null, + /** + * A list of sender IDs to exclude. If this list is absent then no senders are excluded. + * A matching sender will be excluded even if it is listed in the 'senders' filter. + */ + @Json(name = "not_senders") val notSenders: List? = null, + /** + * A list of event types to include. If this list is absent then all event types are included. + * A '*' can be used as a wildcard to match any sequence of characters. + */ + @Json(name = "types") val types: List? = null, + /** + * A list of event types to exclude. If this list is absent then no event types are excluded. + * A matching type will be excluded even if it is listed in the 'types' filter. + * A '*' can be used as a wildcard to match any sequence of characters. + */ + @Json(name = "not_types") val notTypes: List? = null +) { + fun hasData(): Boolean { + return limit != null + || senders != null + || notSenders != null + || types != null + || notTypes != null + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/Filter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/Filter.kt index fc0472e32f..4b826a00f8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/Filter.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/Filter.kt @@ -17,28 +17,42 @@ package im.vector.matrix.android.internal.session.filter import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.di.MoshiProvider /** - * Represents "Filter" as mentioned in the SPEC + * Class which can be parsed to a filter json string. Used for POST and GET + * Have a look here for further information: * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter */ @JsonClass(generateAdapter = true) -data class Filter( - @Json(name = "limit") val limit: Int? = null, - @Json(name = "senders") val senders: List? = null, - @Json(name = "not_senders") val notSenders: List? = null, - @Json(name = "types") val types: List? = null, - @Json(name = "not_types") val notTypes: List? = null, - @Json(name = "rooms") val rooms: List? = null, - @Json(name = "not_rooms") val notRooms: List? = null +internal data class Filter( + /** + * List of event fields to include. If this list is absent then all fields are included. The entries may + * include '.' characters to indicate sub-fields. So ['content.body'] will include the 'body' field of the + * 'content' object. A literal '.' character in a field name may be escaped using a '\'. A server may + * include more fields than were requested. + */ + @Json(name = "event_fields") val eventFields: List? = null, + /** + * The format to use for events. 'client' will return the events in a format suitable for clients. + * 'federation' will return the raw event as received over federation. The default is 'client'. One of: ["client", "federation"] + */ + @Json(name = "event_format") val eventFormat: String? = null, + /** + * The presence updates to include. + */ + @Json(name = "presence") val presence: EventFilter? = null, + /** + * The user account data that isn't associated with rooms to include. + */ + @Json(name = "account_data") val accountData: EventFilter? = null, + /** + * Filters to be applied to room data. + */ + @Json(name = "room") val room: RoomFilter? = null ) { - fun hasData(): Boolean { - return (limit != null - || senders != null - || notSenders != null - || types != null - || notTypes != null - || rooms != null - || notRooms != null) + + fun toJSONString(): String { + return MoshiProvider.providesMoshi().adapter(Filter::class.java).toJson(this) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterApi.kt index 092038ee5d..deae2d5b3a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterApi.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterApi.kt @@ -32,7 +32,7 @@ internal interface FilterApi { * @param body the Json representation of a FilterBody object */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter") - fun uploadFilter(@Path("userId") userId: String, @Body body: FilterBody): Call + fun uploadFilter(@Path("userId") userId: String, @Body body: Filter): Call /** * Gets a filter with a given filterId from the homeserver @@ -42,6 +42,5 @@ internal interface FilterApi { * @return Filter */ @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter/{filterId}") - fun getFilterById(@Path("userId") userId: String, @Path("filterId") - filterId: String): Call + fun getFilterById(@Path("userId") userId: String, @Path("filterId") filterId: String): Call } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterBody.kt deleted file mode 100644 index 535c66f637..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterBody.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package im.vector.matrix.android.internal.session.filter - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import im.vector.matrix.android.internal.di.MoshiProvider - -/** - * Class which can be parsed to a filter json string. Used for POST and GET - * Have a look here for further information: - * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter - */ -@JsonClass(generateAdapter = true) -internal data class FilterBody( - @Json(name = "event_fields") val eventFields: List? = null, - @Json(name = "event_format") val eventFormat: String? = null, - @Json(name = "presence") val presence: Filter? = null, - @Json(name = "account_data") val accountData: Filter? = null, - @Json(name = "room") val room: RoomFilter? = null -) { - - fun toJSONString(): String { - return MoshiProvider.providesMoshi().adapter(FilterBody::class.java).toJson(this) - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterFactory.kt index a070759de9..15fa2f3c31 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterFactory.kt @@ -20,12 +20,12 @@ import im.vector.matrix.android.api.session.events.model.EventType internal object FilterFactory { - fun createDefaultFilterBody(): FilterBody { - return FilterUtil.enableLazyLoading(FilterBody(), true) + fun createDefaultFilter(): Filter { + return FilterUtil.enableLazyLoading(Filter(), true) } - fun createRiotFilterBody(): FilterBody { - return FilterBody( + fun createRiotFilter(): Filter { + return Filter( room = RoomFilter( timeline = createRiotTimelineFilter(), state = createRiotStateFilter() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterRepository.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterRepository.kt index d205ea8a87..c558732f44 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterRepository.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterRepository.kt @@ -21,12 +21,12 @@ internal interface FilterRepository { /** * Return true if the filterBody has changed, or need to be sent to the server */ - suspend fun storeFilter(filterBody: FilterBody, roomEventFilter: RoomEventFilter): Boolean + suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean /** * Set the filterId of this filter */ - suspend fun storeFilterId(filterBody: FilterBody, filterId: String) + suspend fun storeFilterId(filter: Filter, filterId: String) /** * Return filter json or filter id diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterResponse.kt index 75e2c23da9..a9bfb70d5e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterResponse.kt @@ -24,5 +24,10 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) data class FilterResponse( + /** + * Required. The ID of the filter that was created. Cannot start with a { as this character + * is used to determine if the filter provided is inline JSON or a previously declared + * filter by homeservers on some APIs. + */ @Json(name = "filter_id") val filterId: String ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt index 3f4e61e6b5..53ede5ad45 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterUtil.kt @@ -81,30 +81,30 @@ internal object FilterUtil { } */ /** - * Compute a new filterBody to enable or disable the lazy loading + * Compute a new filter to enable or disable the lazy loading * * - * If lazy loading is on, the filterBody will looks like + * If lazy loading is on, the filter will looks like * {"room":{"state":{"lazy_load_members":true})} * - * @param filterBody filterBody to patch + * @param filter filter to patch * @param useLazyLoading true to enable lazy loading */ - fun enableLazyLoading(filterBody: FilterBody, useLazyLoading: Boolean): FilterBody { + fun enableLazyLoading(filter: Filter, useLazyLoading: Boolean): Filter { if (useLazyLoading) { // Enable lazy loading - return filterBody.copy( - room = filterBody.room?.copy( - state = filterBody.room.state?.copy(lazyLoadMembers = true) + return filter.copy( + room = filter.room?.copy( + state = filter.room.state?.copy(lazyLoadMembers = true) ?: RoomEventFilter(lazyLoadMembers = true) ) ?: RoomFilter(state = RoomEventFilter(lazyLoadMembers = true)) ) } else { - val newRoomEventFilter = filterBody.room?.state?.copy(lazyLoadMembers = null)?.takeIf { it.hasData() } - val newRoomFilter = filterBody.room?.copy(state = newRoomEventFilter)?.takeIf { it.hasData() } + val newRoomEventFilter = filter.room?.state?.copy(lazyLoadMembers = null)?.takeIf { it.hasData() } + val newRoomFilter = filter.room?.copy(state = newRoomEventFilter)?.takeIf { it.hasData() } - return filterBody.copy( + return filter.copy( room = newRoomFilter ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomEventFilter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomEventFilter.kt index 9cdccc5c8b..6f2de03b90 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomEventFilter.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomEventFilter.kt @@ -25,14 +25,46 @@ import im.vector.matrix.android.internal.di.MoshiProvider */ @JsonClass(generateAdapter = true) data class RoomEventFilter( + /** + * The maximum number of events to return. + */ @Json(name = "limit") var limit: Int? = null, + /** + * A list of sender IDs to exclude. If this list is absent then no senders are excluded. A matching sender will + * be excluded even if it is listed in the 'senders' filter. + */ @Json(name = "not_senders") val notSenders: List? = null, + /** + * A list of event types to exclude. If this list is absent then no event types are excluded. A matching type will + * be excluded even if it is listed in the 'types' filter. A '*' can be used as a wildcard to match any sequence of characters. + */ @Json(name = "not_types") val notTypes: List? = null, + /** + * A list of senders IDs to include. If this list is absent then all senders are included. + */ @Json(name = "senders") val senders: List? = null, + /** + * A list of event types to include. If this list is absent then all event types are included. A '*' can be used as + * a wildcard to match any sequence of characters. + */ @Json(name = "types") val types: List? = null, + /** + * A list of room IDs to include. If this list is absent then all rooms are included. + */ @Json(name = "rooms") val rooms: List? = null, + /** + * A list of room IDs to exclude. If this list is absent then no rooms are excluded. A matching room will be excluded + * even if it is listed in the 'rooms' filter. + */ @Json(name = "not_rooms") val notRooms: List? = null, + /** + * If true, includes only events with a url key in their content. If false, excludes those events. If omitted, url + * key is not considered for filtering. + */ @Json(name = "contains_url") val containsUrl: Boolean? = null, + /** + * If true, enables lazy-loading of membership events. See Lazy-loading room members for more information. Defaults to false. + */ @Json(name = "lazy_load_members") val lazyLoadMembers: Boolean? = null ) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomFilter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomFilter.kt index 3109763570..e79a0a624e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomFilter.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomFilter.kt @@ -24,12 +24,37 @@ import com.squareup.moshi.JsonClass */ @JsonClass(generateAdapter = true) data class RoomFilter( + /** + * A list of room IDs to exclude. If this list is absent then no rooms are excluded. + * A matching room will be excluded even if it is listed in the 'rooms' filter. + * This filter is applied before the filters in ephemeral, state, timeline or account_data + */ @Json(name = "not_rooms") val notRooms: List? = null, + /** + * A list of room IDs to include. If this list is absent then all rooms are included. + * This filter is applied before the filters in ephemeral, state, timeline or account_data + */ @Json(name = "rooms") val rooms: List? = null, + /** + * The events that aren't recorded in the room history, e.g. typing and receipts, to include for rooms. + */ @Json(name = "ephemeral") val ephemeral: RoomEventFilter? = null, + /** + * Include rooms that the user has left in the sync, default false + */ @Json(name = "include_leave") val includeLeave: Boolean? = null, + /** + * The state events to include for rooms. + * Developer remark: StateFilter is exactly the same than RoomEventFilter + */ @Json(name = "state") val state: RoomEventFilter? = null, + /** + * The message and state update events to include for rooms. + */ @Json(name = "timeline") val timeline: RoomEventFilter? = null, + /** + * The per user account data to include for rooms. + */ @Json(name = "account_data") val accountData: RoomEventFilter? = null ) { From d2598480c823d1bdd518a90c71b35a92728534fc Mon Sep 17 00:00:00 2001 From: Weblate Date: Mon, 18 May 2020 22:43:40 +0200 Subject: [PATCH 02/17] var -> val --- .../matrix/android/internal/session/filter/RoomEventFilter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomEventFilter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomEventFilter.kt index 6f2de03b90..81e7b672b4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomEventFilter.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/RoomEventFilter.kt @@ -28,7 +28,7 @@ data class RoomEventFilter( /** * The maximum number of events to return. */ - @Json(name = "limit") var limit: Int? = null, + @Json(name = "limit") val limit: Int? = null, /** * A list of sender IDs to exclude. If this list is absent then no senders are excluded. A matching sender will * be excluded even if it is listed in the 'senders' filter. From 8a9498bae45c1361e2a98c8d80f7e53f975c9090 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 18 May 2020 23:34:42 +0200 Subject: [PATCH 03/17] Uploads: add the service and the task --- .../matrix/android/api/session/room/Room.kt | 2 + .../session/room/uploads/GetUploadsResult.kt | 26 +++++++++ .../session/room/uploads/UploadsService.kt | 35 +++++++++++ .../internal/session/filter/FilterFactory.kt | 9 +++ .../internal/session/room/DefaultRoom.kt | 3 + .../internal/session/room/RoomFactory.kt | 3 + .../internal/session/room/RoomModule.kt | 5 ++ .../room/uploads/DefaultUploadsService.kt | 44 ++++++++++++++ .../session/room/uploads/GetUploadsTask.kt | 58 +++++++++++++++++++ 9 files changed, 185 insertions(+) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadsService.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/DefaultUploadsService.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index 0c3316e802..2fd7d84f04 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.session.room.typing.TypingService +import im.vector.matrix.android.api.session.room.uploads.UploadsService import im.vector.matrix.android.api.util.Optional /** @@ -42,6 +43,7 @@ interface Room : TypingService, MembershipService, StateService, + UploadsService, ReportingService, RelationService, RoomCryptoService, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt new file mode 100644 index 0000000000..b1e70932af --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.uploads + +import im.vector.matrix.android.api.session.events.model.Event + +data class GetUploadsResult( + // List of fetched Events + val events: List, + // token to get more events, or null if there is no more event to fetch + val nextToken: String? +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadsService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadsService.kt new file mode 100644 index 0000000000..54a87cdcd9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadsService.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.uploads + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable + +/** + * This interface defines methods to get event with uploads (= attachments) sent to a room. It's implemented at the room level. + */ +interface UploadsService { + + /** + * Get a list of events containing URL sent to a room, from most recent to oldest one + * @param numberOfEvents the expected number of events to retrieve. The result can contain less events. + * @param since token to get next page, or null to get the first page + */ + fun getUploads(numberOfEvents: Int, + since: String?, + callback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterFactory.kt index 15fa2f3c31..15c57ab1c8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterFactory.kt @@ -20,6 +20,15 @@ import im.vector.matrix.android.api.session.events.model.EventType internal object FilterFactory { + fun createUploadsFilter(numberOfEvents: Int): RoomEventFilter { + return RoomEventFilter( + limit = numberOfEvents, + containsUrl = true, + types = listOf(EventType.MESSAGE), + lazyLoadMembers = true + ) + } + fun createDefaultFilter(): Filter { return FilterUtil.enableLazyLoading(Filter(), true) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index 833469909f..40d0500a48 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -34,6 +34,7 @@ import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.session.room.typing.TypingService +import im.vector.matrix.android.api.session.room.uploads.UploadsService import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM @@ -54,6 +55,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, private val sendService: SendService, private val draftService: DraftService, private val stateService: StateService, + private val uploadsService: UploadsService, private val reportingService: ReportingService, private val readService: ReadService, private val typingService: TypingService, @@ -68,6 +70,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, SendService by sendService, DraftService by draftService, StateService by stateService, + UploadsService by uploadsService, ReportingService by reportingService, ReadService by readService, TypingService by typingService, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 7c544d64cf..974c30dba9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.session.room.state.DefaultStateService import im.vector.matrix.android.internal.session.room.state.SendStateTask import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService import im.vector.matrix.android.internal.session.room.typing.DefaultTypingService +import im.vector.matrix.android.internal.session.room.uploads.DefaultUploadsService import im.vector.matrix.android.internal.task.TaskExecutor import javax.inject.Inject @@ -47,6 +48,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona private val sendServiceFactory: DefaultSendService.Factory, private val draftServiceFactory: DefaultDraftService.Factory, private val stateServiceFactory: DefaultStateService.Factory, + private val uploadsServiceFactory: DefaultUploadsService.Factory, private val reportingServiceFactory: DefaultReportingService.Factory, private val readServiceFactory: DefaultReadService.Factory, private val typingServiceFactory: DefaultTypingService.Factory, @@ -66,6 +68,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona sendService = sendServiceFactory.create(roomId), draftService = draftServiceFactory.create(roomId), stateService = stateServiceFactory.create(roomId), + uploadsService = uploadsServiceFactory.create(roomId), reportingService = reportingServiceFactory.create(roomId), readService = readServiceFactory.create(roomId), typingService = typingServiceFactory.create(roomId), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index b0a60480e3..001ce120c8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -64,6 +64,8 @@ import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEvent import im.vector.matrix.android.internal.session.room.timeline.PaginationTask import im.vector.matrix.android.internal.session.room.typing.DefaultSendTypingTask import im.vector.matrix.android.internal.session.room.typing.SendTypingTask +import im.vector.matrix.android.internal.session.room.uploads.DefaultGetUploadsTask +import im.vector.matrix.android.internal.session.room.uploads.GetUploadsTask import retrofit2.Retrofit @Module @@ -156,4 +158,7 @@ internal abstract class RoomModule { @Binds abstract fun bindSendTypingTask(task: DefaultSendTypingTask): SendTypingTask + + @Binds + abstract fun bindGetUploadsTask(task: DefaultGetUploadsTask): GetUploadsTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/DefaultUploadsService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/DefaultUploadsService.kt new file mode 100644 index 0000000000..e54a3b93fe --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/DefaultUploadsService.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.uploads + +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.room.uploads.GetUploadsResult +import im.vector.matrix.android.api.session.room.uploads.UploadsService +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith + +internal class DefaultUploadsService @AssistedInject constructor( + @Assisted private val roomId: String, + private val taskExecutor: TaskExecutor, + private val getUploadsTask: GetUploadsTask +) : UploadsService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): UploadsService + } + + override fun getUploads(numberOfEvents: Int, since: String?, callback: MatrixCallback): Cancelable { + return getUploadsTask + .configureWith(GetUploadsTask.Params(roomId, numberOfEvents, since)) + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt new file mode 100644 index 0000000000..4bed591315 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.uploads + +import im.vector.matrix.android.api.session.room.uploads.GetUploadsResult +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.filter.FilterFactory +import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection +import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse +import im.vector.matrix.android.internal.session.sync.SyncTokenStore +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface GetUploadsTask : Task { + + data class Params( + val roomId: String, + val numberOfEvents: Int, + val since: String? + ) +} + +internal class DefaultGetUploadsTask @Inject constructor( + private val roomAPI: RoomAPI, + private val tokenStore: SyncTokenStore, + private val eventBus: EventBus) + : GetUploadsTask { + + override suspend fun execute(params: GetUploadsTask.Params): GetUploadsResult { + val since = params.since ?: tokenStore.getLastToken() ?: throw IllegalStateException("No token available") + + val filter = FilterFactory.createUploadsFilter(params.numberOfEvents).toJSONString() + val chunk = executeRequest(eventBus) { + apiCall = roomAPI.getRoomMessagesFrom(params.roomId, since, PaginationDirection.BACKWARDS.value, params.numberOfEvents, filter) + } + + return GetUploadsResult( + events = chunk.events, // reverse? + nextToken = chunk.end?.takeIf { it != chunk.start } + ) + } +} From 0992e768000a5cd992b0ff068b6c18e7800cdb14 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 May 2020 01:39:04 +0200 Subject: [PATCH 04/17] Uploads: add screen - WIP --- vector/build.gradle | 1 + .../im/vector/riotx/core/di/FragmentModule.kt | 20 ++- .../roomprofile/RoomProfileActivity.kt | 3 +- .../roomprofile/uploads/RoomUploadsAction.kt | 21 ++++ .../uploads/RoomUploadsFragment.kt | 75 +++++++++++ .../uploads/RoomUploadsPagerAdapter.kt | 59 +++++++++ .../uploads/RoomUploadsViewModel.kt | 118 ++++++++++++++++++ .../uploads/RoomUploadsViewState.kt | 40 ++++++ .../uploads/child/RoomUploadsFilesFragment.kt | 33 +++++ .../uploads/child/RoomUploadsMediaFragment.kt | 33 +++++ .../main/res/layout/fragment_room_uploads.xml | 85 +++++++++++++ vector/src/main/res/values/strings.xml | 5 + 12 files changed, 491 insertions(+), 2 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsPagerAdapter.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewState.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/child/RoomUploadsFilesFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/child/RoomUploadsMediaFragment.kt create mode 100644 vector/src/main/res/layout/fragment_room_uploads.xml diff --git a/vector/build.gradle b/vector/build.gradle index 74fc96a425..b9a7fbcdc8 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -282,6 +282,7 @@ dependencies { implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0" implementation "com.squareup.moshi:moshi-adapters:$moshi_version" + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" // Log diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index bc526f93a5..b7b0a1c2f4 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -76,6 +76,9 @@ import im.vector.riotx.features.roommemberprofile.devices.DeviceTrustInfoActionF import im.vector.riotx.features.roomprofile.RoomProfileFragment import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment +import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment +import im.vector.riotx.features.roomprofile.uploads.child.RoomUploadsFilesFragment +import im.vector.riotx.features.roomprofile.uploads.child.RoomUploadsMediaFragment import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment import im.vector.riotx.features.settings.VectorSettingsLabsFragment @@ -96,9 +99,9 @@ import im.vector.riotx.features.settings.locale.LocalePickerFragment import im.vector.riotx.features.settings.push.PushGatewaysFragment import im.vector.riotx.features.share.IncomingShareFragment import im.vector.riotx.features.signout.soft.SoftLogoutFragment +import im.vector.riotx.features.terms.ReviewTermsFragment import im.vector.riotx.features.userdirectory.KnownUsersFragment import im.vector.riotx.features.userdirectory.UserDirectoryFragment -import im.vector.riotx.features.terms.ReviewTermsFragment @Module interface FragmentModule { @@ -308,6 +311,21 @@ interface FragmentModule { @FragmentKey(RoomMemberListFragment::class) fun bindRoomMemberListFragment(fragment: RoomMemberListFragment): Fragment + @Binds + @IntoMap + @FragmentKey(RoomUploadsFragment::class) + fun bindRoomUploadsFragment(fragment: RoomUploadsFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(RoomUploadsMediaFragment::class) + fun bindRoomUploadsMediaFragment(fragment: RoomUploadsMediaFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(RoomUploadsFilesFragment::class) + fun bindRoomUploadsFilesFragment(fragment: RoomUploadsFilesFragment): Fragment + @Binds @IntoMap @FragmentKey(RoomSettingsFragment::class) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileActivity.kt index 1a9b268b90..bfc815f1ed 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileActivity.kt @@ -27,6 +27,7 @@ import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment +import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment class RoomProfileActivity : VectorBaseActivity(), ToolbarConfigurable { @@ -66,7 +67,7 @@ class RoomProfileActivity : VectorBaseActivity(), ToolbarConfigurable { } private fun openRoomUploads() { - notImplemented("Open room uploads") + addFragmentToBackstack(R.id.simpleFragmentContainer, RoomUploadsFragment::class.java, roomProfileArgs) } private fun openRoomSettings() { diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt new file mode 100644 index 0000000000..5191db30a6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomprofile.uploads + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class RoomUploadsAction : VectorViewModelAction diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt new file mode 100644 index 0000000000..0a35d4cf08 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomprofile.uploads + +import android.os.Bundle +import android.view.View +import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.roomprofile.RoomProfileArgs +import kotlinx.android.synthetic.main.fragment_room_uploads.* +import javax.inject.Inject + +class RoomUploadsFragment @Inject constructor( + private val viewModelFactory: RoomUploadsViewModel.Factory, + private val stringProvider: StringProvider, + private val avatarRenderer: AvatarRenderer +) : VectorBaseFragment(), RoomUploadsViewModel.Factory by viewModelFactory { + + private val roomProfileArgs: RoomProfileArgs by args() + + private val viewModel: RoomUploadsViewModel by fragmentViewModel() + + override fun getLayoutResId() = R.layout.fragment_room_uploads + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val sectionsPagerAdapter = RoomUploadsPagerAdapter(childFragmentManager, stringProvider) + view_pager.adapter = sectionsPagerAdapter + tabs.setupWithViewPager(view_pager) + + setupToolbar(matrixProfileToolbar) + + // Initialize your view, subscribe to viewModel... + } + + /* + override fun onDestroyView() { + super.onDestroyView() + // Clear your view, unsubscribe... + } + + */ + + override fun invalidate() = withState(viewModel) { state -> + renderRoomSummary(state) + } + + private fun renderRoomSummary(state: RoomUploadsViewState) { + state.roomSummary()?.let { + matrixProfileToolbarTitleView.text = it.displayName + avatarRenderer.render(it.toMatrixItem(), matrixProfileToolbarAvatarImageView) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsPagerAdapter.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsPagerAdapter.kt new file mode 100644 index 0000000000..ae173a687a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsPagerAdapter.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomprofile.uploads + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentPagerAdapter +import im.vector.riotx.R +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.roomprofile.uploads.child.RoomUploadsFilesFragment +import im.vector.riotx.features.roomprofile.uploads.child.RoomUploadsMediaFragment + +private val TAB_TITLES = arrayOf( + R.string.uploads_title_media, + R.string.uploads_title_files +) + +/** + * A [FragmentPagerAdapter] that returns a fragment corresponding to + * one of the sections/tabs/pages. + */ +class RoomUploadsPagerAdapter( + fm: FragmentManager, + private val stringProvider: StringProvider +) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + + override fun getItem(position: Int): Fragment { + // getItem is called to instantiate the fragment for the given page. + // Return a PlaceholderFragment (defined as a static inner class below). + return if (position == 0) { + RoomUploadsMediaFragment() + } else { + RoomUploadsFilesFragment() + } + } + + override fun getPageTitle(position: Int): CharSequence? { + return stringProvider.getString(TAB_TITLES[position]) + } + + override fun getCount(): Int { + // Show 2 total pages. + return 2 + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt new file mode 100644 index 0000000000..3cf402bccf --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomprofile.uploads + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.room.uploads.GetUploadsResult +import im.vector.matrix.android.internal.util.awaitCallback +import im.vector.matrix.rx.rx +import im.vector.matrix.rx.unwrap +import im.vector.riotx.core.platform.EmptyViewEvents +import im.vector.riotx.core.platform.VectorViewModel +import kotlinx.coroutines.launch + +class RoomUploadsViewModel @AssistedInject constructor( + @Assisted initialState: RoomUploadsViewState, + private val session: Session +) : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: RoomUploadsViewState): RoomUploadsViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: RoomUploadsViewState): RoomUploadsViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + private val room = session.getRoom(initialState.roomId)!! + + init { + observeRoomSummary() + // Send a first request + handleLoadMore() + } + + private fun observeRoomSummary() { + room.rx().liveRoomSummary() + .unwrap() + .execute { async -> + copy(roomSummary = async) + } + } + + private fun handleLoadMore() = withState { state -> + if (state.asyncEventsRequest is Loading) return@withState + + setState { + copy( + asyncEventsRequest = Loading() + ) + } + + viewModelScope.launch { + try { + val result = awaitCallback { + room.getUploads(20, token, it) + } + + token = result.nextToken + + setState { + copy( + asyncEventsRequest = Uninitialized, + events = this.events + result.events, + hasMore = result.nextToken != null + ) + } + } catch (failure: Throwable) { + // TODO Post fail + setState { + copy( + asyncEventsRequest = Fail(failure) + ) + } + } + } + } + + private var token: String? = null + + override fun handle(action: RoomUploadsAction) { + // when (action) { +// + // }.exhaustive + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewState.kt new file mode 100644 index 0000000000..93b1b3814a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewState.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomprofile.uploads + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.riotx.features.roomprofile.RoomProfileArgs + +data class RoomUploadsViewState( + val roomId: String = "", + val roomSummary: Async = Uninitialized, + // Store cumul of pagination result, grouped by type + val mediaEvents: List = emptyList(), + val fileEvents: List = emptyList(), + // Current pagination request + val asyncEventsRequest: Async> = Uninitialized, + // True if more result are available server side + val hasMore: Boolean = false +) : MvRxState { + + constructor(args: RoomProfileArgs) : this(roomId = args.roomId) +} + diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/child/RoomUploadsFilesFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/child/RoomUploadsFilesFragment.kt new file mode 100644 index 0000000000..4f583ca7a1 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/child/RoomUploadsFilesFragment.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomprofile.uploads.child + +import com.airbnb.mvrx.parentFragmentViewModel +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel +import javax.inject.Inject + +/** + * A placeholder fragment containing a simple view. + */ +class RoomUploadsFilesFragment @Inject constructor() : VectorBaseFragment() { + + private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class) + + override fun getLayoutResId() = R.layout.fragment_generic_recycler +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/child/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/child/RoomUploadsMediaFragment.kt new file mode 100644 index 0000000000..eab4adab1c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/child/RoomUploadsMediaFragment.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomprofile.uploads.child + +import com.airbnb.mvrx.parentFragmentViewModel +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel +import javax.inject.Inject + +/** + * A placeholder fragment containing a simple view. + */ +class RoomUploadsMediaFragment @Inject constructor() : VectorBaseFragment() { + + private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class) + + override fun getLayoutResId() = R.layout.fragment_generic_recycler +} diff --git a/vector/src/main/res/layout/fragment_room_uploads.xml b/vector/src/main/res/layout/fragment_room_uploads.xml new file mode 100644 index 0000000000..d3cbf08603 --- /dev/null +++ b/vector/src/main/res/layout/fragment_room_uploads.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 0a9faa8bce..f5d161d901 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1788,6 +1788,11 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming "Sticker" Couldn\'t handle share data + MEDIA + There is no media in this room + FILES + There is no files in this room + "It's spam" "It's inappropriate" "Custom report…" From e9ca876444f31212b419ed2416a3f0d83b90eb4c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 May 2020 17:39:10 +0200 Subject: [PATCH 05/17] Uploads: add screen - WIP --- .../android/api/session/events/model/Event.kt | 17 ++ .../session/room/uploads/GetUploadsResult.kt | 2 +- .../room/uploads/DefaultUploadsService.kt | 4 +- .../session/room/uploads/GetUploadsTask.kt | 2 +- .../im/vector/riotx/core/di/FragmentModule.kt | 4 +- .../epoxy/SquareLoadingItem.kt} | 17 +- .../riotx/core/utils/DimensionConverter.kt | 4 + .../home/room/detail/RoomDetailFragment.kt | 29 +-- .../features/media/ImageContentRenderer.kt | 11 ++ .../features/navigation/DefaultNavigator.kt | 30 ++++ .../riotx/features/navigation/Navigator.kt | 7 + .../roomprofile/uploads/RoomUploadsAction.kt | 8 +- .../uploads/RoomUploadsFragment.kt | 19 +- .../uploads/RoomUploadsPagerAdapter.kt | 42 ++--- .../uploads/RoomUploadsViewModel.kt | 11 +- .../uploads/files/RoomUploadsFilesFragment.kt | 73 ++++++++ .../uploads/files/UploadsFileController.kt | 102 +++++++++++ .../uploads/files/UploadsFileItem.kt | 57 ++++++ .../Config.kt} | 19 +- .../uploads/media/RoomUploadsMediaFragment.kt | 82 +++++++++ .../uploads/media/UploadsImageItem.kt | 49 ++++++ .../uploads/media/UploadsMediaController.kt | 165 ++++++++++++++++++ .../uploads/media/UploadsVideoItem.kt | 50 ++++++ .../main/res/layout/fragment_room_uploads.xml | 22 +-- .../main/res/layout/item_loading_square.xml | 23 +++ .../src/main/res/layout/item_uploads_file.xml | 75 ++++++++ .../main/res/layout/item_uploads_image.xml | 22 +++ .../main/res/layout/item_uploads_video.xml | 31 ++++ 28 files changed, 868 insertions(+), 109 deletions(-) rename vector/src/main/java/im/vector/riotx/{features/roomprofile/uploads/child/RoomUploadsFilesFragment.kt => core/epoxy/SquareLoadingItem.kt} (53%) create mode 100644 vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileItem.kt rename vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/{child/RoomUploadsMediaFragment.kt => media/Config.kt} (50%) create mode 100644 vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt create mode 100644 vector/src/main/res/layout/item_loading_square.xml create mode 100644 vector/src/main/res/layout/item_uploads_file.xml create mode 100644 vector/src/main/res/layout/item_uploads_image.xml create mode 100644 vector/src/main/res/layout/item_uploads_video.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index a60d0fd9ac..3ea3345814 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -220,3 +220,20 @@ fun Event.isImageMessage(): Boolean { else -> false } } + +fun Event.isVideoMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel()?.msgType) { + MessageType.MSGTYPE_VIDEO -> true + else -> false + } +} + +fun Event.isPreviewableMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel()?.msgType) { + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_VIDEO -> true + else -> false + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt index b1e70932af..3b151d5701 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.uploads import im.vector.matrix.android.api.session.events.model.Event data class GetUploadsResult( - // List of fetched Events + // List of fetched Events, most recent first val events: List, // token to get more events, or null if there is no more event to fetch val nextToken: String? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/DefaultUploadsService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/DefaultUploadsService.kt index e54a3b93fe..dd8269a079 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/DefaultUploadsService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/DefaultUploadsService.kt @@ -38,7 +38,9 @@ internal class DefaultUploadsService @AssistedInject constructor( override fun getUploads(numberOfEvents: Int, since: String?, callback: MatrixCallback): Cancelable { return getUploadsTask - .configureWith(GetUploadsTask.Params(roomId, numberOfEvents, since)) + .configureWith(GetUploadsTask.Params(roomId, numberOfEvents, since)) { + this.callback = callback + } .executeBy(taskExecutor) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt index 4bed591315..647fc57fff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt @@ -51,7 +51,7 @@ internal class DefaultGetUploadsTask @Inject constructor( } return GetUploadsResult( - events = chunk.events, // reverse? + events = chunk.events, nextToken = chunk.end?.takeIf { it != chunk.start } ) } diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index b7b0a1c2f4..7e0407fef4 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -77,8 +77,8 @@ import im.vector.riotx.features.roomprofile.RoomProfileFragment import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment -import im.vector.riotx.features.roomprofile.uploads.child.RoomUploadsFilesFragment -import im.vector.riotx.features.roomprofile.uploads.child.RoomUploadsMediaFragment +import im.vector.riotx.features.roomprofile.uploads.files.RoomUploadsFilesFragment +import im.vector.riotx.features.roomprofile.uploads.media.RoomUploadsMediaFragment import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment import im.vector.riotx.features.settings.VectorSettingsLabsFragment diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/child/RoomUploadsFilesFragment.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/SquareLoadingItem.kt similarity index 53% rename from vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/child/RoomUploadsFilesFragment.kt rename to vector/src/main/java/im/vector/riotx/core/epoxy/SquareLoadingItem.kt index 4f583ca7a1..c0f6eb198f 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/child/RoomUploadsFilesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/SquareLoadingItem.kt @@ -14,20 +14,13 @@ * limitations under the License. */ -package im.vector.riotx.features.roomprofile.uploads.child +package im.vector.riotx.core.epoxy -import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R -import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel -import javax.inject.Inject -/** - * A placeholder fragment containing a simple view. - */ -class RoomUploadsFilesFragment @Inject constructor() : VectorBaseFragment() { +@EpoxyModelClass(layout = R.layout.item_loading_square) +abstract class SquareLoadingItem : VectorEpoxyModel() { - private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class) - - override fun getLayoutResId() = R.layout.fragment_generic_recycler + class Holder : VectorEpoxyHolder() } diff --git a/vector/src/main/java/im/vector/riotx/core/utils/DimensionConverter.kt b/vector/src/main/java/im/vector/riotx/core/utils/DimensionConverter.kt index 826d9a495a..4c2ff29874 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/DimensionConverter.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/DimensionConverter.kt @@ -36,4 +36,8 @@ class DimensionConverter @Inject constructor(val resources: Resources) { resources.displayMetrics ).toInt() } + + fun pdToDp(px: Int): Int { + return (px.toFloat() / resources.displayMetrics.density).toInt() + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 079a2927d7..25c59e8f41 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -22,7 +22,6 @@ import android.content.DialogInterface import android.content.Intent import android.graphics.Typeface import android.net.Uri -import android.os.Build import android.os.Bundle import android.os.Parcelable import android.text.Spannable @@ -30,12 +29,10 @@ import android.view.HapticFeedbackConstants import android.view.Menu import android.view.MenuItem import android.view.View -import android.view.Window import android.widget.Toast import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog -import androidx.core.app.ActivityOptionsCompat import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.core.text.buildSpannedString @@ -150,7 +147,6 @@ import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.media.ImageContentRenderer -import im.vector.riotx.features.media.ImageMediaViewerActivity import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.media.VideoMediaViewerActivity import im.vector.riotx.features.notifications.NotificationDrawerManager @@ -998,31 +994,14 @@ class RoomDetailFragment @Inject constructor( } override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) { - // TODO Use navigator - - val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view)) - val pairs = ArrayList>() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - requireActivity().window.decorView.findViewById(android.R.id.statusBarBackground)?.let { - pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) - } - requireActivity().window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { - pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) - } + navigator.openImageViewer(requireActivity(), mediaData, view) { pairs -> + pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: "")) + pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: "")) } - pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) - pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: "")) - pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: "")) - - val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( - requireActivity(), *pairs.toTypedArray()).toBundle() - startActivity(intent, bundle) } override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) { - // TODO Use navigator - val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData) - startActivity(intent) + navigator.openVideoViewer(requireActivity(), mediaData) } override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) { diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt index becb714bf4..6756024aff 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt @@ -65,6 +65,17 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: STICKER } + /** + * For gallery + */ + fun render(data: Data, imageView: ImageView, size: Int) { + // a11y + imageView.contentDescription = data.filename + + createGlideRequest(data, Mode.THUMBNAIL, imageView, Size(size, size)) + .into(imageView) + } + fun render(data: Data, mode: Mode, imageView: ImageView) { val size = processSize(data, mode) imageView.layoutParams.width = size.width diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index b2213eb223..db12bcf61b 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -19,9 +19,12 @@ package im.vector.riotx.features.navigation import android.app.Activity import android.content.Context import android.content.Intent +import android.os.Build import android.view.View +import android.view.Window import androidx.core.app.ActivityOptionsCompat import androidx.core.app.TaskStackBuilder +import androidx.core.util.Pair import androidx.core.view.ViewCompat import androidx.fragment.app.Fragment import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction @@ -45,6 +48,10 @@ import im.vector.riotx.features.home.room.detail.RoomDetailArgs import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.invite.InviteUsersToRoomActivity import im.vector.riotx.features.media.BigImageViewerActivity +import im.vector.riotx.features.media.ImageContentRenderer +import im.vector.riotx.features.media.ImageMediaViewerActivity +import im.vector.riotx.features.media.VideoContentRenderer +import im.vector.riotx.features.media.VideoMediaViewerActivity import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity @@ -215,6 +222,29 @@ class DefaultNavigator @Inject constructor( fragment.startActivityForResult(intent, requestCode) } + override fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?) { + val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view)) + val pairs = ArrayList>() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + activity.window.decorView.findViewById(android.R.id.statusBarBackground)?.let { + pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)) + } + activity.window.decorView.findViewById(android.R.id.navigationBarBackground)?.let { + pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)) + } + } + pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: "")) + options?.invoke(pairs) + + val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle() + activity.startActivity(intent, bundle) + } + + override fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) { + val intent = VideoMediaViewerActivity.newIntent(activity, mediaData) + activity.startActivity(intent) + } + private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) { if (buildTask) { val stackBuilder = TaskStackBuilder.create(context) diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index 07ec0e4ca2..9323a87da5 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -19,10 +19,13 @@ package im.vector.riotx.features.navigation import android.app.Activity import android.content.Context import android.view.View +import androidx.core.util.Pair import androidx.fragment.app.Fragment import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.util.MatrixItem +import im.vector.riotx.features.media.ImageContentRenderer +import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.share.SharedData import im.vector.riotx.features.terms.ReviewTermsActivity @@ -76,4 +79,8 @@ interface Navigator { baseUrl: String, token: String?, requestCode: Int = ReviewTermsActivity.TERMS_REQUEST_CODE) + + fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList>) -> Unit)?) + + fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt index 5191db30a6..24cb4f6bcb 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt @@ -16,6 +16,12 @@ package im.vector.riotx.features.roomprofile.uploads +import im.vector.matrix.android.api.session.events.model.Event import im.vector.riotx.core.platform.VectorViewModelAction -sealed class RoomUploadsAction : VectorViewModelAction +sealed class RoomUploadsAction : VectorViewModelAction { + data class Download(val event: Event) : RoomUploadsAction() + data class Share(val event: Event) : RoomUploadsAction() + + object Retry : RoomUploadsAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt index 0a35d4cf08..632965c864 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt @@ -21,6 +21,7 @@ import android.view.View import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import com.google.android.material.tabs.TabLayoutMediator import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.platform.VectorBaseFragment @@ -45,11 +46,17 @@ class RoomUploadsFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val sectionsPagerAdapter = RoomUploadsPagerAdapter(childFragmentManager, stringProvider) - view_pager.adapter = sectionsPagerAdapter - tabs.setupWithViewPager(view_pager) + val sectionsPagerAdapter = RoomUploadsPagerAdapter(this) + roomUploadsViewPager.adapter = sectionsPagerAdapter - setupToolbar(matrixProfileToolbar) + TabLayoutMediator(roomUploadsTabs, roomUploadsViewPager) { tab, position -> + when (position) { + 0 -> tab.text = stringProvider.getString(R.string.uploads_media_title) + 1 -> tab.text = stringProvider.getString(R.string.uploads_files_title) + } + }.attach() + + setupToolbar(roomUploadsToolbar) // Initialize your view, subscribe to viewModel... } @@ -68,8 +75,8 @@ class RoomUploadsFragment @Inject constructor( private fun renderRoomSummary(state: RoomUploadsViewState) { state.roomSummary()?.let { - matrixProfileToolbarTitleView.text = it.displayName - avatarRenderer.render(it.toMatrixItem(), matrixProfileToolbarAvatarImageView) + roomUploadsToolbarTitleView.text = it.displayName + avatarRenderer.render(it.toMatrixItem(), roomUploadsToolbarAvatarImageView) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsPagerAdapter.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsPagerAdapter.kt index ae173a687a..6866d5e2fa 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsPagerAdapter.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsPagerAdapter.kt @@ -17,43 +17,21 @@ package im.vector.riotx.features.roomprofile.uploads import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.fragment.app.FragmentPagerAdapter -import im.vector.riotx.R -import im.vector.riotx.core.resources.StringProvider -import im.vector.riotx.features.roomprofile.uploads.child.RoomUploadsFilesFragment -import im.vector.riotx.features.roomprofile.uploads.child.RoomUploadsMediaFragment +import androidx.viewpager2.adapter.FragmentStateAdapter +import im.vector.riotx.features.roomprofile.uploads.files.RoomUploadsFilesFragment +import im.vector.riotx.features.roomprofile.uploads.media.RoomUploadsMediaFragment -private val TAB_TITLES = arrayOf( - R.string.uploads_title_media, - R.string.uploads_title_files -) - -/** - * A [FragmentPagerAdapter] that returns a fragment corresponding to - * one of the sections/tabs/pages. - */ class RoomUploadsPagerAdapter( - fm: FragmentManager, - private val stringProvider: StringProvider -) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + private val fragment: Fragment +) : FragmentStateAdapter(fragment) { - override fun getItem(position: Int): Fragment { - // getItem is called to instantiate the fragment for the given page. - // Return a PlaceholderFragment (defined as a static inner class below). + override fun getItemCount() = 2 + + override fun createFragment(position: Int): Fragment { return if (position == 0) { - RoomUploadsMediaFragment() + fragment.childFragmentManager.fragmentFactory.instantiate(fragment.requireContext().classLoader, RoomUploadsMediaFragment::class.java.name) } else { - RoomUploadsFilesFragment() + fragment.childFragmentManager.fragmentFactory.instantiate(fragment.requireContext().classLoader, RoomUploadsFilesFragment::class.java.name) } } - - override fun getPageTitle(position: Int): CharSequence? { - return stringProvider.getString(TAB_TITLES[position]) - } - - override fun getCount(): Int { - // Show 2 total pages. - return 2 - } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt index 3cf402bccf..5f7072aa58 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt @@ -27,6 +27,10 @@ import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.isPreviewableMessage +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.uploads.GetUploadsResult import im.vector.matrix.android.internal.util.awaitCallback import im.vector.matrix.rx.rx @@ -90,10 +94,15 @@ class RoomUploadsViewModel @AssistedInject constructor( token = result.nextToken + val groupedEvents = result.events + .filter { it.getClearType() == EventType.MESSAGE && it.getClearContent()?.toModel() != null } + .groupBy { it.isPreviewableMessage() } + setState { copy( asyncEventsRequest = Uninitialized, - events = this.events + result.events, + mediaEvents = this.mediaEvents + groupedEvents[true].orEmpty(), + fileEvents = this.fileEvents + groupedEvents[false].orEmpty(), hasMore = result.nextToken != null ) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt new file mode 100644 index 0000000000..4aaddac0bd --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomprofile.uploads.files + +import android.os.Bundle +import android.view.View +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.features.roomprofile.uploads.RoomUploadsAction +import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel +import kotlinx.android.synthetic.main.fragment_generic_recycler.* +import javax.inject.Inject + +class RoomUploadsFilesFragment @Inject constructor( + private val controller: UploadsFileController +) : VectorBaseFragment(), UploadsFileController.Listener { + + private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class) + + override fun getLayoutResId() = R.layout.fragment_generic_recycler + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + recyclerView.configureWith(controller, showDivider = true) + controller.listener = this + } + + override fun onDestroyView() { + super.onDestroyView() + recyclerView.cleanup() + controller.listener = null + } + + override fun onOpenClicked(event: Event) { + // TODO + } + + override fun onRetry() { + uploadsViewModel.handle(RoomUploadsAction.Retry) + } + + override fun onDownloadClicked(event: Event) { + uploadsViewModel.handle(RoomUploadsAction.Download(event)) + } + + override fun onShareClicked(event: Event) { + uploadsViewModel.handle(RoomUploadsAction.Share(event)) + } + + override fun invalidate() = withState(uploadsViewModel) { state -> + controller.setData(state) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt new file mode 100644 index 0000000000..b5f5785d69 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomprofile.uploads.files + +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.errorWithRetryItem +import im.vector.riotx.core.epoxy.loadingItem +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewState +import javax.inject.Inject + +class UploadsFileController @Inject constructor( + private val errorFormatter: ErrorFormatter, + colorProvider: ColorProvider +) : TypedEpoxyController() { + + interface Listener { + fun onRetry() + fun onOpenClicked(event: Event) + fun onDownloadClicked(event: Event) + fun onShareClicked(event: Event) + } + + private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) + + var listener: Listener? = null + + init { + setData(null) + } + + override fun buildModels(data: RoomUploadsViewState?) { + data ?: return + + if (data.fileEvents.isEmpty()) { + when (data.asyncEventsRequest) { + is Loading -> { + loadingItem { + id("loading") + } + } + is Fail -> { + errorWithRetryItem { + id("error") + text(errorFormatter.toHumanReadable(data.asyncEventsRequest.error)) + listener { listener?.onRetry() } + } + } + } + } else { + buildFileItems(data.fileEvents) + + if (data.hasMore) { + loadingItem { + id("loadMore") + } + } + } + } + + private fun buildFileItems(fileEvents: List) { + fileEvents.forEach { + uploadsFileItem { + id(it.eventId ?: "") + title(it.getClearType()) + subtitle(it.getSenderKey()) + listener(object : UploadsFileItem.Listener { + override fun onItemClicked() { + listener?.onOpenClicked(it) + } + + override fun onDownloadClicked() { + listener?.onDownloadClicked(it) + } + + override fun onShareClicked() { + listener?.onShareClicked(it) + } + }) + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileItem.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileItem.kt new file mode 100644 index 0000000000..927672dd70 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileItem.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomprofile.uploads.files + +import android.view.View +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.core.extensions.setTextOrHide + +@EpoxyModelClass(layout = R.layout.item_uploads_file) +abstract class UploadsFileItem : VectorEpoxyModel() { + + @EpoxyAttribute var title: String? = null + @EpoxyAttribute var subtitle: String? = null + + @EpoxyAttribute var listener: Listener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.view.setOnClickListener { listener?.onItemClicked() } + holder.titleView.text = title + holder.subtitleView.setTextOrHide(subtitle) + holder.downloadView.setOnClickListener { listener?.onDownloadClicked() } + holder.shareView.setOnClickListener { listener?.onShareClicked() } + } + + class Holder : VectorEpoxyHolder() { + val titleView by bind(R.id.uploadsFileTitle) + val subtitleView by bind(R.id.uploadsFileSubtitle) + val downloadView by bind(R.id.uploadsFileActionDownload) + val shareView by bind(R.id.uploadsFileActionShare) + } + + interface Listener { + fun onItemClicked() + fun onDownloadClicked() + fun onShareClicked() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/child/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/Config.kt similarity index 50% rename from vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/child/RoomUploadsMediaFragment.kt rename to vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/Config.kt index eab4adab1c..50d4feff55 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/child/RoomUploadsMediaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/Config.kt @@ -14,20 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.roomprofile.uploads.child +package im.vector.riotx.features.roomprofile.uploads.media -import com.airbnb.mvrx.parentFragmentViewModel -import im.vector.riotx.R -import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel -import javax.inject.Inject - -/** - * A placeholder fragment containing a simple view. - */ -class RoomUploadsMediaFragment @Inject constructor() : VectorBaseFragment() { - - private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class) - - override fun getLayoutResId() = R.layout.fragment_generic_recycler -} +// Min image size. Size will be adjusted at runtime +const val IMAGE_SIZE_DP = 120 diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt new file mode 100644 index 0000000000..3b1fdaec85 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomprofile.uploads.media + +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.View +import androidx.recyclerview.widget.GridLayoutManager +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.utils.DimensionConverter +import im.vector.riotx.features.media.ImageContentRenderer +import im.vector.riotx.features.media.VideoContentRenderer +import im.vector.riotx.features.roomprofile.uploads.RoomUploadsAction +import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel +import kotlinx.android.synthetic.main.fragment_generic_recycler.* +import javax.inject.Inject + +class RoomUploadsMediaFragment @Inject constructor( + private val controller: UploadsMediaController, + private val dimensionConverter: DimensionConverter +) : VectorBaseFragment(), UploadsMediaController.Listener { + + private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class) + + override fun getLayoutResId() = R.layout.fragment_generic_recycler + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + recyclerView.layoutManager = GridLayoutManager(context, getNumberOfColumns()) + recyclerView.adapter = controller.adapter + recyclerView.setHasFixedSize(true) + + controller.listener = this + } + + private fun getNumberOfColumns(): Int { + val displayMetrics = DisplayMetrics() + requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) + return dimensionConverter.pdToDp(displayMetrics.widthPixels) / IMAGE_SIZE_DP + } + + override fun onDestroyView() { + super.onDestroyView() + recyclerView.cleanup() + controller.listener = null + } + + override fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) { + navigator.openImageViewer(requireActivity(), mediaData, view, null) + } + + override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) { + navigator.openVideoViewer(requireActivity(), mediaData) + } + + override fun onRetry() { + uploadsViewModel.handle(RoomUploadsAction.Retry) + } + + override fun invalidate() = withState(uploadsViewModel) { state -> + controller.setData(state) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt new file mode 100644 index 0000000000..98026901cc --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsImageItem.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomprofile.uploads.media + +import android.view.View +import android.widget.ImageView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.features.media.ImageContentRenderer + +@EpoxyModelClass(layout = R.layout.item_uploads_image) +abstract class UploadsImageItem : VectorEpoxyModel() { + + @EpoxyAttribute lateinit var imageContentRenderer: ImageContentRenderer + @EpoxyAttribute lateinit var data: ImageContentRenderer.Data + + @EpoxyAttribute var listener: Listener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) } + imageContentRenderer.render(data, holder.imageView, IMAGE_SIZE_DP) + } + + class Holder : VectorEpoxyHolder() { + val imageView by bind(R.id.uploadsImagePreview) + } + + interface Listener { + fun onItemClicked(view: View, data: ImageContentRenderer.Data) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt new file mode 100644 index 0000000000..c80798790f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomprofile.uploads.media + +import android.view.View +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.isImageMessage +import im.vector.matrix.android.api.session.events.model.isVideoMessage +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent +import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent +import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.getFileUrl +import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.errorWithRetryItem +import im.vector.riotx.core.epoxy.squareLoadingItem +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.utils.DimensionConverter +import im.vector.riotx.features.media.ImageContentRenderer +import im.vector.riotx.features.media.VideoContentRenderer +import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewState +import javax.inject.Inject + +class UploadsMediaController @Inject constructor( + private val errorFormatter: ErrorFormatter, + private val imageContentRenderer: ImageContentRenderer, + private val dimensionConverter: DimensionConverter, + colorProvider: ColorProvider +) : TypedEpoxyController() { + + interface Listener { + fun onRetry() + fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) + fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) + } + + private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) + + var listener: Listener? = null + + private val itemSize = dimensionConverter.dpToPx(64) + + init { + setData(null) + } + + override fun buildModels(data: RoomUploadsViewState?) { + data ?: return + + if (data.mediaEvents.isEmpty()) { + when (data.asyncEventsRequest) { + is Loading -> { + squareLoadingItem { + id("loading") + } + } + is Fail -> { + errorWithRetryItem { + id("error") + text(errorFormatter.toHumanReadable(data.asyncEventsRequest.error)) + listener { listener?.onRetry() } + } + } + } + } else { + buildMediaItems(data.mediaEvents) + + if (data.hasMore) { + squareLoadingItem { + id("loadMore") + } + } + } + } + + private fun buildMediaItems(mediaEvents: List) { + mediaEvents.forEach { event -> + when { + event.isImageMessage() -> { + val data = event.toImageContentRendererData() ?: return@forEach + uploadsImageItem { + id(event.eventId ?: "") + imageContentRenderer(imageContentRenderer) + data(data) + listener(object : UploadsImageItem.Listener { + override fun onItemClicked(view: View, data: ImageContentRenderer.Data) { + listener?.onOpenImageClicked(view, data) + } + }) + } + } + event.isVideoMessage() -> { + val data = event.toVideoContentRendererData() ?: return@forEach + uploadsVideoItem { + id(event.eventId ?: "") + imageContentRenderer(imageContentRenderer) + data(data) + listener(object : UploadsVideoItem.Listener { + override fun onItemClicked(view: View, data: VideoContentRenderer.Data) { + listener?.onOpenVideoClicked(view, data) + } + }) + } + } + } + } + } + + private fun Event.toImageContentRendererData(): ImageContentRenderer.Data? { + val messageContent = getClearContent()?.toModel() ?: return null + + return ImageContentRenderer.Data( + eventId = eventId ?: "", + filename = messageContent.body, + url = messageContent.getFileUrl(), + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), + height = messageContent.info?.height, + maxHeight = itemSize, + width = messageContent.info?.width, + maxWidth = itemSize + ) + } + + private fun Event.toVideoContentRendererData(): VideoContentRenderer.Data? { + val messageContent = getClearContent()?.toModel() ?: return null + + val thumbnailData = ImageContentRenderer.Data( + eventId = eventId ?: "", + filename = messageContent.body, + url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl, + elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), + height = messageContent.videoInfo?.height, + maxHeight = itemSize, + width = messageContent.videoInfo?.width, + maxWidth = itemSize + ) + + return VideoContentRenderer.Data( + eventId = eventId ?: "", + filename = messageContent.body, + url = messageContent.getFileUrl(), + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), + thumbnailMediaData = thumbnailData + ) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt new file mode 100644 index 0000000000..82e33b76da --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsVideoItem.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomprofile.uploads.media + +import android.view.View +import android.widget.ImageView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.features.media.ImageContentRenderer +import im.vector.riotx.features.media.VideoContentRenderer + +@EpoxyModelClass(layout = R.layout.item_uploads_video) +abstract class UploadsVideoItem : VectorEpoxyModel() { + + @EpoxyAttribute lateinit var imageContentRenderer: ImageContentRenderer + @EpoxyAttribute lateinit var data: VideoContentRenderer.Data + + @EpoxyAttribute var listener: Listener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) } + imageContentRenderer.render(data.thumbnailMediaData, holder.imageView, IMAGE_SIZE_DP) + } + + class Holder : VectorEpoxyHolder() { + val imageView by bind(R.id.uploadsVideoPreview) + } + + interface Listener { + fun onItemClicked(view: View, data: VideoContentRenderer.Data) + } +} diff --git a/vector/src/main/res/layout/fragment_room_uploads.xml b/vector/src/main/res/layout/fragment_room_uploads.xml index d3cbf08603..36d779b533 100644 --- a/vector/src/main/res/layout/fragment_room_uploads.xml +++ b/vector/src/main/res/layout/fragment_room_uploads.xml @@ -12,7 +12,7 @@ android:elevation="4dp"> @@ -66,18 +66,18 @@ - diff --git a/vector/src/main/res/layout/item_loading_square.xml b/vector/src/main/res/layout/item_loading_square.xml new file mode 100644 index 0000000000..7596c39fc6 --- /dev/null +++ b/vector/src/main/res/layout/item_loading_square.xml @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_uploads_file.xml b/vector/src/main/res/layout/item_uploads_file.xml new file mode 100644 index 0000000000..9232583a28 --- /dev/null +++ b/vector/src/main/res/layout/item_uploads_file.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_uploads_image.xml b/vector/src/main/res/layout/item_uploads_image.xml new file mode 100644 index 0000000000..8ea0506bce --- /dev/null +++ b/vector/src/main/res/layout/item_uploads_image.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_uploads_video.xml b/vector/src/main/res/layout/item_uploads_video.xml new file mode 100644 index 0000000000..6e19326da9 --- /dev/null +++ b/vector/src/main/res/layout/item_uploads_video.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file From 88cba74cacfc60028833584355e5adac26afd6b0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 May 2020 18:43:57 +0200 Subject: [PATCH 06/17] Uploads: add screen - WIP --- .../session/room/uploads/GetUploadsResult.kt | 6 ++-- .../session/room/timeline/TokenChunkEvent.kt | 2 ++ .../room/timeline/TokenChunkEventPersistor.kt | 2 +- .../session/room/uploads/GetUploadsTask.kt | 3 +- .../roomprofile/uploads/RoomUploadsAction.kt | 1 + .../uploads/RoomUploadsViewModel.kt | 17 ++++++---- .../uploads/RoomUploadsViewState.kt | 4 +-- .../uploads/files/RoomUploadsFilesFragment.kt | 7 ++++ .../uploads/files/UploadsFileController.kt | 29 ++++++++++++++--- .../uploads/media/RoomUploadsMediaFragment.kt | 7 ++++ .../uploads/media/UploadsMediaController.kt | 32 +++++++++++++++---- 11 files changed, 88 insertions(+), 22 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt index 3b151d5701..3e194beae5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt @@ -21,6 +21,8 @@ import im.vector.matrix.android.api.session.events.model.Event data class GetUploadsResult( // List of fetched Events, most recent first val events: List, - // token to get more events, or null if there is no more event to fetch - val nextToken: String? + // token to get more events + val nextToken: String, + // True if there are more event to load + val hasMore: Boolean ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt index 95edf9bc49..7344f5598b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEvent.kt @@ -23,4 +23,6 @@ internal interface TokenChunkEvent { val end: String? val events: List val stateEvents: List + + fun hasMore() = start != end } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index f7411b3bf1..e0f5b106d6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -149,7 +149,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy } ?: ChunkEntity.create(realm, prevToken, nextToken) - if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) { + if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) { handleReachEnd(realm, roomId, direction, currentChunk) } else { handlePagination(realm, roomId, direction, receivedChunk, currentChunk) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt index 647fc57fff..d51c72046f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt @@ -52,7 +52,8 @@ internal class DefaultGetUploadsTask @Inject constructor( return GetUploadsResult( events = chunk.events, - nextToken = chunk.end?.takeIf { it != chunk.start } + nextToken = chunk.end ?: "", + hasMore = chunk.hasMore() ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt index 24cb4f6bcb..d817beb3e2 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt @@ -24,4 +24,5 @@ sealed class RoomUploadsAction : VectorViewModelAction { data class Share(val event: Event) : RoomUploadsAction() object Retry : RoomUploadsAction() + object LoadMore : RoomUploadsAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt index 5f7072aa58..0af41ff4b9 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt @@ -22,7 +22,7 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject @@ -35,6 +35,7 @@ import im.vector.matrix.android.api.session.room.uploads.GetUploadsResult import im.vector.matrix.android.internal.util.awaitCallback import im.vector.matrix.rx.rx import im.vector.matrix.rx.unwrap +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.EmptyViewEvents import im.vector.riotx.core.platform.VectorViewModel import kotlinx.coroutines.launch @@ -79,6 +80,7 @@ class RoomUploadsViewModel @AssistedInject constructor( private fun handleLoadMore() = withState { state -> if (state.asyncEventsRequest is Loading) return@withState + if (!state.hasMore) return@withState setState { copy( @@ -100,10 +102,10 @@ class RoomUploadsViewModel @AssistedInject constructor( setState { copy( - asyncEventsRequest = Uninitialized, + asyncEventsRequest = Success(Unit), mediaEvents = this.mediaEvents + groupedEvents[true].orEmpty(), fileEvents = this.fileEvents + groupedEvents[false].orEmpty(), - hasMore = result.nextToken != null + hasMore = result.hasMore ) } } catch (failure: Throwable) { @@ -120,8 +122,11 @@ class RoomUploadsViewModel @AssistedInject constructor( private var token: String? = null override fun handle(action: RoomUploadsAction) { - // when (action) { -// - // }.exhaustive + when (action) { + is RoomUploadsAction.Download -> TODO() + is RoomUploadsAction.Share -> TODO() + RoomUploadsAction.Retry -> handleLoadMore() + RoomUploadsAction.LoadMore -> handleLoadMore() + }.exhaustive } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewState.kt index 93b1b3814a..31c4e937c8 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewState.kt @@ -30,9 +30,9 @@ data class RoomUploadsViewState( val mediaEvents: List = emptyList(), val fileEvents: List = emptyList(), // Current pagination request - val asyncEventsRequest: Async> = Uninitialized, + val asyncEventsRequest: Async = Uninitialized, // True if more result are available server side - val hasMore: Boolean = false + val hasMore: Boolean = true ) : MvRxState { constructor(args: RoomProfileArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt index 4aaddac0bd..10f3625545 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.roomprofile.uploads.files import android.os.Bundle import android.view.View +import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState import im.vector.matrix.android.api.session.events.model.Event @@ -41,6 +42,8 @@ class RoomUploadsFilesFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val epoxyVisibilityTracker = EpoxyVisibilityTracker() + epoxyVisibilityTracker.attach(recyclerView) recyclerView.configureWith(controller, showDivider = true) controller.listener = this } @@ -59,6 +62,10 @@ class RoomUploadsFilesFragment @Inject constructor( uploadsViewModel.handle(RoomUploadsAction.Retry) } + override fun loadMore() { + uploadsViewModel.handle(RoomUploadsAction.LoadMore) + } + override fun onDownloadClicked(event: Event) { uploadsViewModel.handle(RoomUploadsAction.Download(event)) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt index b5f5785d69..e27be153c8 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt @@ -17,31 +17,33 @@ package im.vector.riotx.features.roomprofile.uploads.files import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.epoxy.VisibilityState import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success import im.vector.matrix.android.api.session.events.model.Event import im.vector.riotx.R import im.vector.riotx.core.epoxy.errorWithRetryItem import im.vector.riotx.core.epoxy.loadingItem +import im.vector.riotx.core.epoxy.noResultItem import im.vector.riotx.core.error.ErrorFormatter -import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewState import javax.inject.Inject class UploadsFileController @Inject constructor( private val errorFormatter: ErrorFormatter, - colorProvider: ColorProvider + private val stringProvider: StringProvider ) : TypedEpoxyController() { interface Listener { fun onRetry() + fun loadMore() fun onOpenClicked(event: Event) fun onDownloadClicked(event: Event) fun onShareClicked(event: Event) } - private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) - var listener: Listener? = null init { @@ -65,6 +67,20 @@ class UploadsFileController @Inject constructor( listener { listener?.onRetry() } } } + is Success -> { + if (data.hasMore) { + // We need to load more items + listener?.loadMore() + loadingItem { + id("loading") + } + } else { + noResultItem { + id("noResult") + text(stringProvider.getString(R.string.uploads_files_no_result)) + } + } + } } } else { buildFileItems(data.fileEvents) @@ -72,6 +88,11 @@ class UploadsFileController @Inject constructor( if (data.hasMore) { loadingItem { id("loadMore") + onVisibilityStateChanged { _, _, visibilityState -> + if (visibilityState == VisibilityState.VISIBLE) { + listener?.loadMore() + } + } } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt index 3b1fdaec85..0446778a11 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt @@ -20,6 +20,7 @@ import android.os.Bundle import android.util.DisplayMetrics import android.view.View import androidx.recyclerview.widget.GridLayoutManager +import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R @@ -45,6 +46,8 @@ class RoomUploadsMediaFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val epoxyVisibilityTracker = EpoxyVisibilityTracker() + epoxyVisibilityTracker.attach(recyclerView) recyclerView.layoutManager = GridLayoutManager(context, getNumberOfColumns()) recyclerView.adapter = controller.adapter recyclerView.setHasFixedSize(true) @@ -72,6 +75,10 @@ class RoomUploadsMediaFragment @Inject constructor( navigator.openVideoViewer(requireActivity(), mediaData) } + override fun loadMore() { + uploadsViewModel.handle(RoomUploadsAction.LoadMore) + } + override fun onRetry() { uploadsViewModel.handle(RoomUploadsAction.Retry) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt index c80798790f..41711359d2 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt @@ -18,22 +18,24 @@ package im.vector.riotx.features.roomprofile.uploads.media import android.view.View import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.epoxy.VisibilityState import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.isImageMessage import im.vector.matrix.android.api.session.events.model.isVideoMessage import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageImageContent -import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.riotx.R import im.vector.riotx.core.epoxy.errorWithRetryItem +import im.vector.riotx.core.epoxy.noResultItem import im.vector.riotx.core.epoxy.squareLoadingItem import im.vector.riotx.core.error.ErrorFormatter -import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer @@ -43,18 +45,17 @@ import javax.inject.Inject class UploadsMediaController @Inject constructor( private val errorFormatter: ErrorFormatter, private val imageContentRenderer: ImageContentRenderer, - private val dimensionConverter: DimensionConverter, - colorProvider: ColorProvider + private val stringProvider: StringProvider, + dimensionConverter: DimensionConverter ) : TypedEpoxyController() { interface Listener { fun onRetry() fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) + fun loadMore() } - private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) - var listener: Listener? = null private val itemSize = dimensionConverter.dpToPx(64) @@ -80,6 +81,20 @@ class UploadsMediaController @Inject constructor( listener { listener?.onRetry() } } } + is Success -> { + if (data.hasMore) { + // We need to load more items + listener?.loadMore() + squareLoadingItem { + id("loading") + } + } else { + noResultItem { + id("noResult") + text(stringProvider.getString(R.string.uploads_media_no_result)) + } + } + } } } else { buildMediaItems(data.mediaEvents) @@ -87,6 +102,11 @@ class UploadsMediaController @Inject constructor( if (data.hasMore) { squareLoadingItem { id("loadMore") + onVisibilityStateChanged { _, _, visibilityState -> + if (visibilityState == VisibilityState.VISIBLE) { + listener?.loadMore() + } + } } } } From 919225bdfd56dd7ca65b9b9c2c9d0ffd5a1d7413 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 May 2020 18:49:29 +0200 Subject: [PATCH 07/17] Uploads: create extension --- .../java/im/vector/riotx/core/extensions/RecyclerView.kt | 3 +++ .../riotx/features/home/room/detail/RoomDetailFragment.kt | 6 ++---- .../riotx/features/roomdirectory/PublicRoomsFragment.kt | 5 ++--- .../roomprofile/uploads/files/RoomUploadsFilesFragment.kt | 5 ++--- .../roomprofile/uploads/media/RoomUploadsMediaFragment.kt | 5 ++--- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt b/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt index 3b3132229c..3762c52d45 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt @@ -21,6 +21,7 @@ import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyController +import com.airbnb.epoxy.EpoxyVisibilityTracker import im.vector.riotx.R import im.vector.riotx.features.themes.ThemeUtils @@ -61,3 +62,5 @@ fun RecyclerView.configureWith(epoxyController: EpoxyController, fun RecyclerView.cleanup() { adapter = null } + +fun RecyclerView.trackItemsVisibilityChange() = EpoxyVisibilityTracker().attach(this) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 25c59e8f41..bd9a4c9d7e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -46,7 +46,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import com.airbnb.epoxy.EpoxyModel -import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail @@ -92,6 +91,7 @@ import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.extensions.showKeyboard +import im.vector.riotx.core.extensions.trackItemsVisibilityChange import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.intent.getMimeTypeFromUri @@ -148,7 +148,6 @@ import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer -import im.vector.riotx.features.media.VideoMediaViewerActivity import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.permalink.NavigationInterceptor import im.vector.riotx.features.permalink.PermalinkHandler @@ -539,8 +538,7 @@ class RoomDetailFragment @Inject constructor( timelineEventController.callback = this timelineEventController.timeline = roomDetailViewModel.timeline - val epoxyVisibilityTracker = EpoxyVisibilityTracker() - epoxyVisibilityTracker.attach(recyclerView) + recyclerView.trackItemsVisibilityChange() layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController) diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt index a75479275b..869ee85337 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt @@ -19,7 +19,6 @@ package im.vector.riotx.features.roomdirectory import android.os.Bundle import android.view.MenuItem import android.view.View -import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState import com.google.android.material.snackbar.Snackbar @@ -29,6 +28,7 @@ import im.vector.riotx.R import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.extensions.trackItemsVisibilityChange import im.vector.riotx.core.platform.VectorBaseFragment import io.reactivex.rxkotlin.subscribeBy import kotlinx.android.synthetic.main.fragment_public_rooms.* @@ -107,8 +107,7 @@ class PublicRoomsFragment @Inject constructor( } private fun setupRecyclerView() { - val epoxyVisibilityTracker = EpoxyVisibilityTracker() - epoxyVisibilityTracker.attach(publicRoomsList) + publicRoomsList.trackItemsVisibilityChange() publicRoomsList.configureWith(publicRoomsController) publicRoomsController.callback = this } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt index 10f3625545..1a27fa7b12 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt @@ -18,13 +18,13 @@ package im.vector.riotx.features.roomprofile.uploads.files import android.os.Bundle import android.view.View -import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState import im.vector.matrix.android.api.session.events.model.Event import im.vector.riotx.R import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.extensions.trackItemsVisibilityChange import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.features.roomprofile.uploads.RoomUploadsAction import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel @@ -42,8 +42,7 @@ class RoomUploadsFilesFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val epoxyVisibilityTracker = EpoxyVisibilityTracker() - epoxyVisibilityTracker.attach(recyclerView) + recyclerView.trackItemsVisibilityChange() recyclerView.configureWith(controller, showDivider = true) controller.listener = this } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt index 0446778a11..5ac31168ad 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt @@ -20,11 +20,11 @@ import android.os.Bundle import android.util.DisplayMetrics import android.view.View import androidx.recyclerview.widget.GridLayoutManager -import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.trackItemsVisibilityChange import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.features.media.ImageContentRenderer @@ -46,8 +46,7 @@ class RoomUploadsMediaFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val epoxyVisibilityTracker = EpoxyVisibilityTracker() - epoxyVisibilityTracker.attach(recyclerView) + recyclerView.trackItemsVisibilityChange() recyclerView.layoutManager = GridLayoutManager(context, getNumberOfColumns()) recyclerView.adapter = controller.adapter recyclerView.setHasFixedSize(true) From f7de2f0f13046326a524ffc6595d40976300b8c0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 19 May 2020 19:26:44 +0200 Subject: [PATCH 08/17] Uploads: create extension --- .../core/utils/ExternalApplicationsUtil.kt | 6 ++- .../roomprofile/uploads/RoomUploadsAction.kt | 5 +- .../uploads/RoomUploadsFragment.kt | 28 +++++++++- .../uploads/RoomUploadsViewEvents.kt | 27 ++++++++++ .../uploads/RoomUploadsViewModel.kt | 51 +++++++++++++++++-- .../uploads/files/RoomUploadsFilesFragment.kt | 11 ++-- .../uploads/files/UploadsFileController.kt | 23 ++++++--- vector/src/main/res/values/strings.xml | 2 + 8 files changed, 133 insertions(+), 20 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewEvents.kt diff --git a/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt index afb7c4586a..e46d756523 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/ExternalApplicationsUtil.kt @@ -256,7 +256,11 @@ fun shareMedia(context: Context, file: File, mediaMimeType: String?) { sendIntent.type = mediaMimeType sendIntent.putExtra(Intent.EXTRA_STREAM, mediaUri) - context.startActivity(sendIntent) + try { + context.startActivity(sendIntent) + } catch (activityNotFoundException: ActivityNotFoundException) { + context.toast(R.string.error_no_external_application_found) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt index d817beb3e2..667e5ab770 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt @@ -17,11 +17,12 @@ package im.vector.riotx.features.roomprofile.uploads import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent import im.vector.riotx.core.platform.VectorViewModelAction sealed class RoomUploadsAction : VectorViewModelAction { - data class Download(val event: Event) : RoomUploadsAction() - data class Share(val event: Event) : RoomUploadsAction() + data class Download(val event: Event, val messageContent: MessageWithAttachmentContent) : RoomUploadsAction() + data class Share(val event: Event, val messageContent: MessageWithAttachmentContent) : RoomUploadsAction() object Retry : RoomUploadsAction() object LoadMore : RoomUploadsAction() diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt index 632965c864..9ef2688b17 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt @@ -18,14 +18,20 @@ package im.vector.riotx.features.roomprofile.uploads import android.os.Bundle import android.view.View +import android.widget.Toast +import androidx.core.net.toUri import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.tabs.TabLayoutMediator import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.intent.getMimeTypeFromUri import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.utils.saveMedia +import im.vector.riotx.core.utils.shareMedia import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.roomprofile.RoomProfileArgs import kotlinx.android.synthetic.main.fragment_room_uploads.* @@ -58,7 +64,27 @@ class RoomUploadsFragment @Inject constructor( setupToolbar(roomUploadsToolbar) - // Initialize your view, subscribe to viewModel... + viewModel.observeViewEvents { + when (it) { + is RoomUploadsViewEvents.FileReadyForSharing -> { + shareMedia(requireContext(), it.file, getMimeTypeFromUri(requireContext(), it.file.toUri())) + } + is RoomUploadsViewEvents.FileReadyForSaving -> { + val saved = saveMedia( + context = requireContext(), + file = it.file, + title = it.title, + mediaMimeType = getMimeTypeFromUri(requireContext(), it.file.toUri()) + ) + if (saved) { + Toast.makeText(requireContext(), R.string.media_file_added_to_gallery, Toast.LENGTH_LONG).show() + } else { + Toast.makeText(requireContext(), R.string.error_adding_media_file_to_gallery, Toast.LENGTH_LONG).show() + } + } + is RoomUploadsViewEvents.Failure -> showFailure(it.throwable) + }.exhaustive + } } /* diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewEvents.kt new file mode 100644 index 0000000000..cd0c34494d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewEvents.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roomprofile.uploads + +import im.vector.riotx.core.platform.VectorViewEvents +import java.io.File + +sealed class RoomUploadsViewEvents : VectorViewEvents { + data class Failure(val throwable: Throwable) : RoomUploadsViewEvents() + + data class FileReadyForSharing(val file: File) : RoomUploadsViewEvents() + data class FileReadyForSaving(val file: File, val title: String) : RoomUploadsViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt index 0af41ff4b9..3239f88ace 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt @@ -30,20 +30,23 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.isPreviewableMessage import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.uploads.GetUploadsResult +import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.util.awaitCallback import im.vector.matrix.rx.rx import im.vector.matrix.rx.unwrap import im.vector.riotx.core.extensions.exhaustive -import im.vector.riotx.core.platform.EmptyViewEvents import im.vector.riotx.core.platform.VectorViewModel import kotlinx.coroutines.launch +import java.io.File class RoomUploadsViewModel @AssistedInject constructor( @Assisted initialState: RoomUploadsViewState, private val session: Session -) : VectorViewModel(initialState) { +) : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { @@ -123,10 +126,50 @@ class RoomUploadsViewModel @AssistedInject constructor( override fun handle(action: RoomUploadsAction) { when (action) { - is RoomUploadsAction.Download -> TODO() - is RoomUploadsAction.Share -> TODO() + is RoomUploadsAction.Download -> handleDownload(action) + is RoomUploadsAction.Share -> handleShare(action) RoomUploadsAction.Retry -> handleLoadMore() RoomUploadsAction.LoadMore -> handleLoadMore() }.exhaustive } + + private fun handleShare(action: RoomUploadsAction.Share) { + viewModelScope.launch { + try { + val file = awaitCallback { + session.downloadFile( + FileService.DownloadMode.FOR_EXTERNAL_SHARE, + action.event.eventId ?: "", + action.messageContent.body, + action.messageContent.getFileUrl(), + action.messageContent.encryptedFileInfo?.toElementToDecrypt(), + it + ) + } + _viewEvents.post(RoomUploadsViewEvents.FileReadyForSharing(file)) + } catch (failure: Throwable) { + _viewEvents.post(RoomUploadsViewEvents.Failure(failure)) + } + } + } + + private fun handleDownload(action: RoomUploadsAction.Download) { + viewModelScope.launch { + try { + val file = awaitCallback { + session.downloadFile( + FileService.DownloadMode.FOR_EXTERNAL_SHARE, + action.event.eventId ?: "", + action.messageContent.body, + action.messageContent.getFileUrl(), + action.messageContent.encryptedFileInfo?.toElementToDecrypt(), + it) + + } + _viewEvents.post(RoomUploadsViewEvents.FileReadyForSaving(file, action.messageContent.body)) + } catch (failure: Throwable) { + _viewEvents.post(RoomUploadsViewEvents.Failure(failure)) + } + } + } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt index 1a27fa7b12..2798cdc683 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt @@ -21,6 +21,7 @@ import android.view.View import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent import im.vector.riotx.R import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.configureWith @@ -54,7 +55,7 @@ class RoomUploadsFilesFragment @Inject constructor( } override fun onOpenClicked(event: Event) { - // TODO + TODO() } override fun onRetry() { @@ -65,12 +66,12 @@ class RoomUploadsFilesFragment @Inject constructor( uploadsViewModel.handle(RoomUploadsAction.LoadMore) } - override fun onDownloadClicked(event: Event) { - uploadsViewModel.handle(RoomUploadsAction.Download(event)) + override fun onDownloadClicked(event: Event, messageWithAttachmentContent: MessageWithAttachmentContent) { + uploadsViewModel.handle(RoomUploadsAction.Download(event, messageWithAttachmentContent)) } - override fun onShareClicked(event: Event) { - uploadsViewModel.handle(RoomUploadsAction.Share(event)) + override fun onShareClicked(event: Event, messageWithAttachmentContent: MessageWithAttachmentContent) { + uploadsViewModel.handle(RoomUploadsAction.Share(event, messageWithAttachmentContent)) } override fun invalidate() = withState(uploadsViewModel) { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt index e27be153c8..e37d2e0c15 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt @@ -22,7 +22,11 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent import im.vector.riotx.R +import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.epoxy.errorWithRetryItem import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.epoxy.noResultItem @@ -33,15 +37,16 @@ import javax.inject.Inject class UploadsFileController @Inject constructor( private val errorFormatter: ErrorFormatter, - private val stringProvider: StringProvider + private val stringProvider: StringProvider, + private val dateFormatter: VectorDateFormatter ) : TypedEpoxyController() { interface Listener { fun onRetry() fun loadMore() fun onOpenClicked(event: Event) - fun onDownloadClicked(event: Event) - fun onShareClicked(event: Event) + fun onDownloadClicked(event: Event, messageWithAttachmentContent: MessageWithAttachmentContent) + fun onShareClicked(event: Event, messageWithAttachmentContent: MessageWithAttachmentContent) } var listener: Listener? = null @@ -100,21 +105,25 @@ class UploadsFileController @Inject constructor( private fun buildFileItems(fileEvents: List) { fileEvents.forEach { + val messageContent = it.getClearContent()?.toModel() ?: return@forEach + val messageWithAttachmentContent = (messageContent as? MessageWithAttachmentContent) ?: return@forEach + uploadsFileItem { id(it.eventId ?: "") - title(it.getClearType()) - subtitle(it.getSenderKey()) + title(messageWithAttachmentContent.body) + // TODO Resolve user displayName + subtitle(stringProvider.getString(R.string.uploads_files_subtitle, it.senderId, dateFormatter.formatRelativeDateTime(it.originServerTs))) listener(object : UploadsFileItem.Listener { override fun onItemClicked() { listener?.onOpenClicked(it) } override fun onDownloadClicked() { - listener?.onDownloadClicked(it) + listener?.onDownloadClicked(it, messageWithAttachmentContent) } override fun onShareClicked() { - listener?.onShareClicked(it) + listener?.onShareClicked(it, messageWithAttachmentContent) } }) } diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index f5d161d901..b0ddce5ed3 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1791,6 +1791,8 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming MEDIA There is no media in this room FILES + + %1$s at %2$s There is no files in this room "It's spam" From a2b366ebfed5dd29e46b0e779d61d478efc541b8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 May 2020 08:56:29 +0200 Subject: [PATCH 09/17] Uploads: add placeholder for images --- .../features/media/ImageContentRenderer.kt | 2 ++ vector/src/main/res/drawable/ic_image.xml | 29 +++++++++++++++++++ .../main/res/layout/item_uploads_image.xml | 2 +- .../main/res/layout/item_uploads_video.xml | 2 +- 4 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 vector/src/main/res/drawable/ic_image.xml diff --git a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt index 6756024aff..ab047fba0d 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/ImageContentRenderer.kt @@ -29,6 +29,7 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.ORIENTATION import com.github.piasy.biv.view.BigImageView import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt +import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideRequest @@ -73,6 +74,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: imageView.contentDescription = data.filename createGlideRequest(data, Mode.THUMBNAIL, imageView, Size(size, size)) + .placeholder(R.drawable.ic_image) .into(imageView) } diff --git a/vector/src/main/res/drawable/ic_image.xml b/vector/src/main/res/drawable/ic_image.xml new file mode 100644 index 0000000000..70bc4a73a6 --- /dev/null +++ b/vector/src/main/res/drawable/ic_image.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/vector/src/main/res/layout/item_uploads_image.xml b/vector/src/main/res/layout/item_uploads_image.xml index 8ea0506bce..464816d74a 100644 --- a/vector/src/main/res/layout/item_uploads_image.xml +++ b/vector/src/main/res/layout/item_uploads_image.xml @@ -11,7 +11,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:layout_margin="2dp" - android:scaleType="centerCrop" + android:scaleType="center" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="H,1:1" app:layout_constraintEnd_toEndOf="parent" diff --git a/vector/src/main/res/layout/item_uploads_video.xml b/vector/src/main/res/layout/item_uploads_video.xml index 6e19326da9..97d7529c13 100644 --- a/vector/src/main/res/layout/item_uploads_video.xml +++ b/vector/src/main/res/layout/item_uploads_video.xml @@ -11,7 +11,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:layout_margin="2dp" - android:scaleType="centerCrop" + android:scaleType="center" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="H,1:1" app:layout_constraintEnd_toEndOf="parent" From e3ed3e5b05983b8fc222e06ef992e7f03d8ebc29 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 May 2020 09:08:42 +0200 Subject: [PATCH 10/17] Uploads: cleanup --- vector/src/main/java/im/vector/riotx/core/ui/model/Size.kt | 4 +++- .../java/im/vector/riotx/core/utils/DimensionConverter.kt | 5 ++++- .../roomprofile/uploads/media/RoomUploadsMediaFragment.kt | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/ui/model/Size.kt b/vector/src/main/java/im/vector/riotx/core/ui/model/Size.kt index 65ab0ad2b2..4a07bb2cea 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/model/Size.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/model/Size.kt @@ -16,5 +16,7 @@ package im.vector.riotx.core.ui.model +import androidx.annotation.Px + // android.util.Size in API 21+ -data class Size(val width: Int, val height: Int) +data class Size(@Px val width: Int, @Px val height: Int) diff --git a/vector/src/main/java/im/vector/riotx/core/utils/DimensionConverter.kt b/vector/src/main/java/im/vector/riotx/core/utils/DimensionConverter.kt index 4c2ff29874..01cd6a4f8f 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/DimensionConverter.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/DimensionConverter.kt @@ -17,10 +17,12 @@ package im.vector.riotx.core.utils import android.content.res.Resources import android.util.TypedValue +import androidx.annotation.Px import javax.inject.Inject class DimensionConverter @Inject constructor(val resources: Resources) { + @Px fun dpToPx(dp: Int): Int { return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, @@ -29,6 +31,7 @@ class DimensionConverter @Inject constructor(val resources: Resources) { ).toInt() } + @Px fun spToPx(sp: Int): Int { return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_SP, @@ -37,7 +40,7 @@ class DimensionConverter @Inject constructor(val resources: Resources) { ).toInt() } - fun pdToDp(px: Int): Int { + fun pxToDp(@Px px: Int): Int { return (px.toFloat() / resources.displayMetrics.density).toInt() } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt index 5ac31168ad..a722c8281f 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt @@ -57,7 +57,7 @@ class RoomUploadsMediaFragment @Inject constructor( private fun getNumberOfColumns(): Int { val displayMetrics = DisplayMetrics() requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) - return dimensionConverter.pdToDp(displayMetrics.widthPixels) / IMAGE_SIZE_DP + return dimensionConverter.pxToDp(displayMetrics.widthPixels) / IMAGE_SIZE_DP } override fun onDestroyView() { From 907a786b1a01d246918fb487fcd7078a32125bd2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 May 2020 09:16:31 +0200 Subject: [PATCH 11/17] Uploads: load element until loader not displayed anymore --- .../roomprofile/uploads/files/UploadsFileController.kt | 5 ++++- .../roomprofile/uploads/media/UploadsMediaController.kt | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt index e37d2e0c15..0ad173be88 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt @@ -51,6 +51,8 @@ class UploadsFileController @Inject constructor( var listener: Listener? = null + private var idx = 0 + init { setData(null) } @@ -92,7 +94,8 @@ class UploadsFileController @Inject constructor( if (data.hasMore) { loadingItem { - id("loadMore") + // Always use a different id, because we can be notified several times of visibility state changed + id("loadMore${idx++}") onVisibilityStateChanged { _, _, visibilityState -> if (visibilityState == VisibilityState.VISIBLE) { listener?.loadMore() diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt index 41711359d2..c6e3666fb5 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt @@ -58,7 +58,9 @@ class UploadsMediaController @Inject constructor( var listener: Listener? = null - private val itemSize = dimensionConverter.dpToPx(64) + private var idx = 0 + + private val itemSize = dimensionConverter.dpToPx(IMAGE_SIZE_DP) init { setData(null) @@ -101,7 +103,8 @@ class UploadsMediaController @Inject constructor( if (data.hasMore) { squareLoadingItem { - id("loadMore") + // Always use a different id, because we can be notified several times of visibility state changed + id("loadMore${idx++}") onVisibilityStateChanged { _, _, visibilityState -> if (visibilityState == VisibilityState.VISIBLE) { listener?.loadMore() From f3a5fb7fe3eda015802873eabbc585239d62e857 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 May 2020 11:56:54 +0200 Subject: [PATCH 12/17] Uploads: rework: provide information about the sender --- .../android/api/session/events/model/Event.kt | 9 ---- .../session/room/uploads/GetUploadsResult.kt | 5 +-- .../api/session/room/uploads/UploadEvent.kt | 31 +++++++++++++ .../session/room/uploads/UploadSenderInfo.kt | 33 ++++++++++++++ .../membership/RoomDisplayNameResolver.kt | 1 + .../session/room/uploads/GetUploadsTask.kt | 44 ++++++++++++++++++- .../roomprofile/uploads/RoomUploadsAction.kt | 7 ++- .../uploads/RoomUploadsViewModel.kt | 29 ++++++------ .../uploads/RoomUploadsViewState.kt | 6 +-- .../uploads/files/RoomUploadsFilesFragment.kt | 16 +++---- .../uploads/files/UploadsFileController.kt | 33 ++++++-------- .../uploads/media/UploadsMediaController.kt | 38 ++++++++-------- 12 files changed, 170 insertions(+), 82 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadEvent.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadSenderInfo.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index 3ea3345814..d3780ebe60 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -228,12 +228,3 @@ fun Event.isVideoMessage(): Boolean { else -> false } } - -fun Event.isPreviewableMessage(): Boolean { - return getClearType() == EventType.MESSAGE - && when (getClearContent()?.toModel()?.msgType) { - MessageType.MSGTYPE_IMAGE, - MessageType.MSGTYPE_VIDEO -> true - else -> false - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt index 3e194beae5..bbe8641edd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt @@ -16,13 +16,12 @@ package im.vector.matrix.android.api.session.room.uploads -import im.vector.matrix.android.api.session.events.model.Event - data class GetUploadsResult( // List of fetched Events, most recent first - val events: List, + val events: List, // token to get more events val nextToken: String, // True if there are more event to load val hasMore: Boolean ) + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadEvent.kt new file mode 100644 index 0000000000..93ce59a233 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadEvent.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.uploads + +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent + +/** + * Wrapper around on Event. + * Similar to [im.vector.matrix.android.api.session.room.timeline.TimelineEvent], contains an Event with extra useful data + */ +data class UploadEvent( + val root: Event, + val eventId: String, + val contentWithAttachmentContent: MessageWithAttachmentContent, + val uploadSenderInfo: UploadSenderInfo +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadSenderInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadSenderInfo.kt new file mode 100644 index 0000000000..0fa4b2cb68 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadSenderInfo.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.uploads + +// TODO Maybe use this model for TimelineEvent as well +data class UploadSenderInfo( + val senderId: String, + val senderName: String?, + val isUniqueDisplayName: Boolean, + val senderAvatar: String? +) { + fun getDisambiguatedDisplayName(): String { + return when { + senderName.isNullOrBlank() -> senderId + isUniqueDisplayName -> senderName + else -> "$senderName (${senderId})" + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt index feb05a3275..e7a68b3b5b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -126,6 +126,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: return name ?: roomId } + /** See [im.vector.matrix.android.api.session.room.timeline.TimelineEvent.getDisambiguatedDisplayName] */ private fun resolveRoomMemberName(roomMemberSummary: RoomMemberSummaryEntity?, roomMemberHelper: RoomMemberHelper): String? { if (roomMemberSummary == null) return null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt index d51c72046f..f6fd2a442b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt @@ -16,10 +16,17 @@ package im.vector.matrix.android.internal.session.room.uploads +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent import im.vector.matrix.android.api.session.room.uploads.GetUploadsResult +import im.vector.matrix.android.api.session.room.uploads.UploadEvent +import im.vector.matrix.android.api.session.room.uploads.UploadSenderInfo import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.filter.FilterFactory import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse import im.vector.matrix.android.internal.session.sync.SyncTokenStore @@ -39,6 +46,7 @@ internal interface GetUploadsTask : Task() + + val cacheOfSenderInfos = mutableMapOf() + + // Get a snapshot of all room members + monarchy.doWithRealm { realm -> + val roomMemberHelper = RoomMemberHelper(realm, params.roomId) + + uploadEvents = chunk.events.mapNotNull { event -> + val eventId = event.eventId ?: return@mapNotNull null + val messageContent = event.getClearContent()?.toModel() ?: return@mapNotNull null + val messageWithAttachmentContent = (messageContent as? MessageWithAttachmentContent) ?: return@mapNotNull null + val senderId = event.senderId ?: return@mapNotNull null + + val senderInfo = cacheOfSenderInfos.getOrPut(senderId) { + val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(senderId) + UploadSenderInfo( + senderId = senderId, + senderName = roomMemberSummaryEntity?.displayName, + isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(roomMemberSummaryEntity?.displayName), + senderAvatar = roomMemberSummaryEntity?.avatarUrl + ) + } + + UploadEvent( + root = event, + eventId = eventId, + contentWithAttachmentContent = messageWithAttachmentContent, + uploadSenderInfo = senderInfo + ) + } + } + + return GetUploadsResult( - events = chunk.events, + events = uploadEvents, nextToken = chunk.end ?: "", hasMore = chunk.hasMore() ) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt index 667e5ab770..59571de122 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt @@ -16,13 +16,12 @@ package im.vector.riotx.features.roomprofile.uploads -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent +import im.vector.matrix.android.api.session.room.uploads.UploadEvent import im.vector.riotx.core.platform.VectorViewModelAction sealed class RoomUploadsAction : VectorViewModelAction { - data class Download(val event: Event, val messageContent: MessageWithAttachmentContent) : RoomUploadsAction() - data class Share(val event: Event, val messageContent: MessageWithAttachmentContent) : RoomUploadsAction() + data class Download(val uploadEvent: UploadEvent) : RoomUploadsAction() + data class Share(val uploadEvent: UploadEvent) : RoomUploadsAction() object Retry : RoomUploadsAction() object LoadMore : RoomUploadsAction() diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt index 3239f88ace..dcb0963ef0 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt @@ -27,11 +27,8 @@ import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.api.session.events.model.isPreviewableMessage -import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.file.FileService -import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.uploads.GetUploadsResult import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt @@ -100,8 +97,10 @@ class RoomUploadsViewModel @AssistedInject constructor( token = result.nextToken val groupedEvents = result.events - .filter { it.getClearType() == EventType.MESSAGE && it.getClearContent()?.toModel() != null } - .groupBy { it.isPreviewableMessage() } + .groupBy { + it.contentWithAttachmentContent.msgType == MessageType.MSGTYPE_IMAGE + || it.contentWithAttachmentContent.msgType == MessageType.MSGTYPE_VIDEO + } setState { copy( @@ -139,10 +138,10 @@ class RoomUploadsViewModel @AssistedInject constructor( val file = awaitCallback { session.downloadFile( FileService.DownloadMode.FOR_EXTERNAL_SHARE, - action.event.eventId ?: "", - action.messageContent.body, - action.messageContent.getFileUrl(), - action.messageContent.encryptedFileInfo?.toElementToDecrypt(), + action.uploadEvent.eventId, + action.uploadEvent.contentWithAttachmentContent.body, + action.uploadEvent.contentWithAttachmentContent.getFileUrl(), + action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(), it ) } @@ -159,14 +158,14 @@ class RoomUploadsViewModel @AssistedInject constructor( val file = awaitCallback { session.downloadFile( FileService.DownloadMode.FOR_EXTERNAL_SHARE, - action.event.eventId ?: "", - action.messageContent.body, - action.messageContent.getFileUrl(), - action.messageContent.encryptedFileInfo?.toElementToDecrypt(), + action.uploadEvent.eventId, + action.uploadEvent.contentWithAttachmentContent.body, + action.uploadEvent.contentWithAttachmentContent.getFileUrl(), + action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(), it) } - _viewEvents.post(RoomUploadsViewEvents.FileReadyForSaving(file, action.messageContent.body)) + _viewEvents.post(RoomUploadsViewEvents.FileReadyForSaving(file, action.uploadEvent.contentWithAttachmentContent.body)) } catch (failure: Throwable) { _viewEvents.post(RoomUploadsViewEvents.Failure(failure)) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewState.kt index 31c4e937c8..bed3b264cd 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewState.kt @@ -19,16 +19,16 @@ package im.vector.riotx.features.roomprofile.uploads import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized -import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.room.uploads.UploadEvent import im.vector.riotx.features.roomprofile.RoomProfileArgs data class RoomUploadsViewState( val roomId: String = "", val roomSummary: Async = Uninitialized, // Store cumul of pagination result, grouped by type - val mediaEvents: List = emptyList(), - val fileEvents: List = emptyList(), + val mediaEvents: List = emptyList(), + val fileEvents: List = emptyList(), // Current pagination request val asyncEventsRequest: Async = Uninitialized, // True if more result are available server side diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt index 2798cdc683..63f9e5215e 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt @@ -20,8 +20,7 @@ import android.os.Bundle import android.view.View import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent +import im.vector.matrix.android.api.session.room.uploads.UploadEvent import im.vector.riotx.R import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.configureWith @@ -54,8 +53,9 @@ class RoomUploadsFilesFragment @Inject constructor( controller.listener = null } - override fun onOpenClicked(event: Event) { - TODO() + override fun onOpenClicked(uploadEvent: UploadEvent) { + // Same action than Share + uploadsViewModel.handle(RoomUploadsAction.Share(uploadEvent)) } override fun onRetry() { @@ -66,12 +66,12 @@ class RoomUploadsFilesFragment @Inject constructor( uploadsViewModel.handle(RoomUploadsAction.LoadMore) } - override fun onDownloadClicked(event: Event, messageWithAttachmentContent: MessageWithAttachmentContent) { - uploadsViewModel.handle(RoomUploadsAction.Download(event, messageWithAttachmentContent)) + override fun onDownloadClicked(uploadEvent: UploadEvent) { + uploadsViewModel.handle(RoomUploadsAction.Download(uploadEvent)) } - override fun onShareClicked(event: Event, messageWithAttachmentContent: MessageWithAttachmentContent) { - uploadsViewModel.handle(RoomUploadsAction.Share(event, messageWithAttachmentContent)) + override fun onShareClicked(uploadEvent: UploadEvent) { + uploadsViewModel.handle(RoomUploadsAction.Share(uploadEvent)) } override fun invalidate() = withState(uploadsViewModel) { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt index 0ad173be88..918fc433c9 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt @@ -21,10 +21,7 @@ import com.airbnb.epoxy.VisibilityState import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.message.MessageContent -import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent +import im.vector.matrix.android.api.session.room.uploads.UploadEvent import im.vector.riotx.R import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.epoxy.errorWithRetryItem @@ -44,9 +41,9 @@ class UploadsFileController @Inject constructor( interface Listener { fun onRetry() fun loadMore() - fun onOpenClicked(event: Event) - fun onDownloadClicked(event: Event, messageWithAttachmentContent: MessageWithAttachmentContent) - fun onShareClicked(event: Event, messageWithAttachmentContent: MessageWithAttachmentContent) + fun onOpenClicked(uploadEvent: UploadEvent) + fun onDownloadClicked(uploadEvent: UploadEvent) + fun onShareClicked(uploadEvent: UploadEvent) } var listener: Listener? = null @@ -106,27 +103,25 @@ class UploadsFileController @Inject constructor( } } - private fun buildFileItems(fileEvents: List) { - fileEvents.forEach { - val messageContent = it.getClearContent()?.toModel() ?: return@forEach - val messageWithAttachmentContent = (messageContent as? MessageWithAttachmentContent) ?: return@forEach - + private fun buildFileItems(fileEvents: List) { + fileEvents.forEach { uploadEvent -> uploadsFileItem { - id(it.eventId ?: "") - title(messageWithAttachmentContent.body) - // TODO Resolve user displayName - subtitle(stringProvider.getString(R.string.uploads_files_subtitle, it.senderId, dateFormatter.formatRelativeDateTime(it.originServerTs))) + id(uploadEvent.eventId) + title(uploadEvent.contentWithAttachmentContent.body) + subtitle(stringProvider.getString(R.string.uploads_files_subtitle, + uploadEvent.uploadSenderInfo.getDisambiguatedDisplayName(), + dateFormatter.formatRelativeDateTime(uploadEvent.root.originServerTs))) listener(object : UploadsFileItem.Listener { override fun onItemClicked() { - listener?.onOpenClicked(it) + listener?.onOpenClicked(uploadEvent) } override fun onDownloadClicked() { - listener?.onDownloadClicked(it, messageWithAttachmentContent) + listener?.onDownloadClicked(uploadEvent) } override fun onShareClicked() { - listener?.onShareClicked(it, messageWithAttachmentContent) + listener?.onShareClicked(uploadEvent) } }) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt index c6e3666fb5..1d797bec78 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt @@ -22,13 +22,11 @@ import com.airbnb.epoxy.VisibilityState import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.isImageMessage -import im.vector.matrix.android.api.session.events.model.isVideoMessage -import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageImageContent +import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.api.session.room.model.message.getFileUrl +import im.vector.matrix.android.api.session.room.uploads.UploadEvent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.riotx.R import im.vector.riotx.core.epoxy.errorWithRetryItem @@ -115,13 +113,13 @@ class UploadsMediaController @Inject constructor( } } - private fun buildMediaItems(mediaEvents: List) { - mediaEvents.forEach { event -> - when { - event.isImageMessage() -> { - val data = event.toImageContentRendererData() ?: return@forEach + private fun buildMediaItems(mediaEvents: List) { + mediaEvents.forEach { uploadEvent -> + when (uploadEvent.contentWithAttachmentContent.msgType) { + MessageType.MSGTYPE_IMAGE -> { + val data = uploadEvent.toImageContentRendererData() ?: return@forEach uploadsImageItem { - id(event.eventId ?: "") + id(uploadEvent.eventId) imageContentRenderer(imageContentRenderer) data(data) listener(object : UploadsImageItem.Listener { @@ -131,10 +129,10 @@ class UploadsMediaController @Inject constructor( }) } } - event.isVideoMessage() -> { - val data = event.toVideoContentRendererData() ?: return@forEach + MessageType.MSGTYPE_VIDEO -> { + val data = uploadEvent.toVideoContentRendererData() ?: return@forEach uploadsVideoItem { - id(event.eventId ?: "") + id(uploadEvent.eventId) imageContentRenderer(imageContentRenderer) data(data) listener(object : UploadsVideoItem.Listener { @@ -148,11 +146,11 @@ class UploadsMediaController @Inject constructor( } } - private fun Event.toImageContentRendererData(): ImageContentRenderer.Data? { - val messageContent = getClearContent()?.toModel() ?: return null + private fun UploadEvent.toImageContentRendererData(): ImageContentRenderer.Data? { + val messageContent = (contentWithAttachmentContent as? MessageImageContent) ?: return null return ImageContentRenderer.Data( - eventId = eventId ?: "", + eventId = eventId, filename = messageContent.body, url = messageContent.getFileUrl(), elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), @@ -163,11 +161,11 @@ class UploadsMediaController @Inject constructor( ) } - private fun Event.toVideoContentRendererData(): VideoContentRenderer.Data? { - val messageContent = getClearContent()?.toModel() ?: return null + private fun UploadEvent.toVideoContentRendererData(): VideoContentRenderer.Data? { + val messageContent = (contentWithAttachmentContent as? MessageVideoContent) ?: return null val thumbnailData = ImageContentRenderer.Data( - eventId = eventId ?: "", + eventId = eventId, filename = messageContent.body, url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl, elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), @@ -178,7 +176,7 @@ class UploadsMediaController @Inject constructor( ) return VideoContentRenderer.Data( - eventId = eventId ?: "", + eventId = eventId, filename = messageContent.body, url = messageContent.getFileUrl(), elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), From 2adafbeb034c4611d31b47efeabe9ddf4eacf3d3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 May 2020 12:19:30 +0200 Subject: [PATCH 13/17] Uploads: use SenderInfo in TimelineEvent --- .../SenderInfo.kt} | 20 +++++++++-------- .../session/room/timeline/TimelineEvent.kt | 13 ++--------- .../api/session/room/uploads/UploadEvent.kt | 3 ++- .../matrix/android/api/util/MatrixItem.kt | 3 +++ .../database/mapper/TimelineEventMapper.kt | 11 ++++++---- .../room/send/LocalEchoEventFactory.kt | 2 +- .../session/room/uploads/GetUploadsTask.kt | 14 ++++++------ .../home/room/detail/RoomDetailFragment.kt | 8 ++----- .../factory/MergedHeaderItemFactory.kt | 12 ++++------ .../format/DisplayableEventFormatter.kt | 2 +- .../timeline/format/NoticeEventFormatter.kt | 22 +++++++++---------- .../helper/MessageInformationDataFactory.kt | 10 ++++----- .../reactions/ViewReactionsViewModel.kt | 2 +- .../notifications/NotifiableEventResolver.kt | 6 ++--- .../uploads/files/UploadsFileController.kt | 2 +- 15 files changed, 60 insertions(+), 70 deletions(-) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/{uploads/UploadSenderInfo.kt => sender/SenderInfo.kt} (63%) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadSenderInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/sender/SenderInfo.kt similarity index 63% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadSenderInfo.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/sender/SenderInfo.kt index 0fa4b2cb68..c3f0df8312 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadSenderInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/sender/SenderInfo.kt @@ -14,20 +14,22 @@ * limitations under the License. */ -package im.vector.matrix.android.api.session.room.uploads +package im.vector.matrix.android.api.session.room.sender -// TODO Maybe use this model for TimelineEvent as well -data class UploadSenderInfo( - val senderId: String, - val senderName: String?, +data class SenderInfo( + val userId: String, + /** + * Consider using [getDisambiguatedDisplayName] + */ + val displayName: String?, val isUniqueDisplayName: Boolean, - val senderAvatar: String? + val avatarUrl: String? ) { fun getDisambiguatedDisplayName(): String { return when { - senderName.isNullOrBlank() -> senderId - isUniqueDisplayName -> senderName - else -> "$senderName (${senderId})" + displayName.isNullOrBlank() -> userId + isUniqueDisplayName -> displayName + else -> "$displayName (${userId})" } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index 7adc438e20..273ea2366a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageStickerContent import im.vector.matrix.android.api.session.room.model.message.isReply +import im.vector.matrix.android.api.session.room.sender.SenderInfo import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent @@ -39,9 +40,7 @@ data class TimelineEvent( val localId: Long, val eventId: String, val displayIndex: Int, - val senderName: String?, - val isUniqueDisplayName: Boolean, - val senderAvatar: String?, + val senderInfo: SenderInfo, val annotations: EventAnnotationsSummary? = null, val readReceipts: List = emptyList() ) { @@ -69,14 +68,6 @@ data class TimelineEvent( } } - fun getDisambiguatedDisplayName(): String { - return when { - senderName.isNullOrBlank() -> root.senderId ?: "" - isUniqueDisplayName -> senderName - else -> "$senderName (${root.senderId})" - } - } - /** * Get the metadata associated with a key. * @param key the key to get the metadata diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadEvent.kt index 93ce59a233..5df2b9c9e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadEvent.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room.uploads import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent +import im.vector.matrix.android.api.session.room.sender.SenderInfo /** * Wrapper around on Event. @@ -27,5 +28,5 @@ data class UploadEvent( val root: Event, val eventId: String, val contentWithAttachmentContent: MessageWithAttachmentContent, - val uploadSenderInfo: UploadSenderInfo + val senderInfo: SenderInfo ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt index d5aa897c7d..95c0435f1b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom +import im.vector.matrix.android.api.session.room.sender.SenderInfo import im.vector.matrix.android.api.session.user.model.User import java.util.Locale @@ -154,3 +155,5 @@ fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlia fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl) fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) + +fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, getDisambiguatedDisplayName(), avatarUrl) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt index 4bd9b9855b..6653c1a448 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/TimelineEventMapper.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.database.mapper import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.ReadReceipt - +import im.vector.matrix.android.api.session.room.sender.SenderInfo import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.database.model.TimelineEventEntity import javax.inject.Inject @@ -41,9 +41,12 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS annotations = timelineEventEntity.annotations?.asDomain(), localId = timelineEventEntity.localId, displayIndex = timelineEventEntity.displayIndex, - senderName = timelineEventEntity.senderName, - isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, - senderAvatar = timelineEventEntity.senderAvatar, + senderInfo = SenderInfo( + userId = timelineEventEntity.root?.sender ?: "", + displayName = timelineEventEntity.senderName, + isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, + avatarUrl = timelineEventEntity.senderAvatar + ), readReceipts = readReceipts ?.distinctBy { it.user diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 2a24094b5d..2d1e2775c0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -202,7 +202,7 @@ internal class LocalEchoEventFactory @Inject constructor( permalink, stringProvider.getString(R.string.message_reply_to_prefix), userLink, - originalEvent.getDisambiguatedDisplayName(), + originalEvent.senderInfo.getDisambiguatedDisplayName(), body.takeFormatted(), createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted() ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt index f6fd2a442b..cc78a02c88 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt @@ -20,9 +20,9 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent +import im.vector.matrix.android.api.session.room.sender.SenderInfo import im.vector.matrix.android.api.session.room.uploads.GetUploadsResult import im.vector.matrix.android.api.session.room.uploads.UploadEvent -import im.vector.matrix.android.api.session.room.uploads.UploadSenderInfo import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.filter.FilterFactory import im.vector.matrix.android.internal.session.room.RoomAPI @@ -60,7 +60,7 @@ internal class DefaultGetUploadsTask @Inject constructor( var uploadEvents = listOf() - val cacheOfSenderInfos = mutableMapOf() + val cacheOfSenderInfos = mutableMapOf() // Get a snapshot of all room members monarchy.doWithRealm { realm -> @@ -74,11 +74,11 @@ internal class DefaultGetUploadsTask @Inject constructor( val senderInfo = cacheOfSenderInfos.getOrPut(senderId) { val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(senderId) - UploadSenderInfo( - senderId = senderId, - senderName = roomMemberSummaryEntity?.displayName, + SenderInfo( + userId = senderId, + displayName = roomMemberSummaryEntity?.displayName, isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(roomMemberSummaryEntity?.displayName), - senderAvatar = roomMemberSummaryEntity?.avatarUrl + avatarUrl = roomMemberSummaryEntity?.avatarUrl ) } @@ -86,7 +86,7 @@ internal class DefaultGetUploadsTask @Inject constructor( root = event, eventId = eventId, contentWithAttachmentContent = messageWithAttachmentContent, - uploadSenderInfo = senderInfo + senderInfo = senderInfo ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index bd9a4c9d7e..9dcf0d42ea 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -458,7 +458,7 @@ class RoomDetailFragment @Inject constructor( autoCompleter.enterSpecialMode() // switch to expanded bar composerLayout.composerRelatedMessageTitle.apply { - text = event.getDisambiguatedDisplayName() + text = event.senderInfo.getDisambiguatedDisplayName() setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.senderId))) } @@ -477,11 +477,7 @@ class RoomDetailFragment @Inject constructor( composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) composerLayout.sendButton.contentDescription = getString(descriptionRes) - avatarRenderer.render( - MatrixItem.UserItem(event.root.senderId - ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar), - composerLayout.composerRelatedMessageAvatar - ) + avatarRenderer.render(event.senderInfo.toMatrixItem(), composerLayout.composerRelatedMessageAvatar) composerLayout.expand { if (isAdded) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 9529693e6b..70d70c2765 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -85,12 +85,10 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) { highlighted = true } - val senderAvatar = mergedEvent.senderAvatar - val senderName = mergedEvent.getDisambiguatedDisplayName() val data = BasedMergedItem.Data( userId = mergedEvent.root.senderId ?: "", - avatarUrl = senderAvatar, - memberName = senderName, + avatarUrl = mergedEvent.senderInfo.avatarUrl, + memberName = mergedEvent.senderInfo.getDisambiguatedDisplayName(), localId = mergedEvent.localId, eventId = mergedEvent.root.eventId ?: "" ) @@ -158,12 +156,10 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) { highlighted = true } - val senderAvatar = mergedEvent.senderAvatar - val senderName = mergedEvent.getDisambiguatedDisplayName() val data = BasedMergedItem.Data( userId = mergedEvent.root.senderId ?: "", - avatarUrl = senderAvatar, - memberName = senderName, + avatarUrl = mergedEvent.senderInfo.avatarUrl, + memberName = mergedEvent.senderInfo.getDisambiguatedDisplayName(), localId = mergedEvent.localId, eventId = mergedEvent.root.eventId ?: "" ) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index 2f7b52de62..ac587a8250 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -45,7 +45,7 @@ class DisplayableEventFormatter @Inject constructor( return stringProvider.getString(R.string.encrypted_message) } - val senderName = timelineEvent.getDisambiguatedDisplayName() + val senderName = timelineEvent.senderInfo.getDisambiguatedDisplayName() when (timelineEvent.root.getClearType()) { EventType.MESSAGE -> { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index f29bd72e0a..99fa25e76f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -47,20 +47,20 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active fun format(timelineEvent: TimelineEvent): CharSequence? { return when (val type = timelineEvent.root.getClearType()) { - EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root) - EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.senderInfo.getDisambiguatedDisplayName()) EventType.CALL_INVITE, EventType.CALL_HANGUP, - EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) + EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) EventType.MESSAGE, EventType.REACTION, EventType.KEY_VERIFICATION_START, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 695da73f89..92374edea7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -64,16 +64,14 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses val showInformation = addDaySeparator - || event.senderAvatar != nextEvent?.senderAvatar - || event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName() + || event.senderInfo.avatarUrl != nextEvent?.senderInfo?.avatarUrl + || event.senderInfo.getDisambiguatedDisplayName() != nextEvent?.senderInfo?.getDisambiguatedDisplayName() || (nextEvent.root.getClearType() != EventType.MESSAGE && nextEvent.root.getClearType() != EventType.ENCRYPTED) || isNextMessageReceivedMoreThanOneHourAgo || isTileTypeMessage(nextEvent) val time = dateFormatter.formatMessageHour(date) - val avatarUrl = event.senderAvatar - val memberName = event.getDisambiguatedDisplayName() - val formattedMemberName = span(memberName) { + val formattedMemberName = span(event.senderInfo.getDisambiguatedDisplayName()) { textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId)) } @@ -85,7 +83,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses sendState = event.root.sendState, time = time, ageLocalTS = event.root.ageLocalTs, - avatarUrl = avatarUrl, + avatarUrl = event.senderInfo.avatarUrl, memberName = formattedMemberName, showInformation = showInformation, orderedReactionList = event.annotations?.reactionsSummary diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt index 05cdbc0fd8..25e407eeb1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt @@ -111,7 +111,7 @@ class ViewReactionsViewModel @AssistedInject constructor(@Assisted event.root.eventId!!, summary.key, event.root.senderId ?: "", - event.getDisambiguatedDisplayName(), + event.senderInfo.getDisambiguatedDisplayName(), dateFormatter.formatRelativeDateTime(event.root.originServerTs) ) diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt index 1f9f54127b..9401c69ad1 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt @@ -93,7 +93,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St // Ok room is not known in store, but we can still display something val body = displayableEventFormatter.format(event, false) val roomName = stringProvider.getString(R.string.notification_unknown_room_name) - val senderDisplayName = event.getDisambiguatedDisplayName() + val senderDisplayName = event.senderInfo.getDisambiguatedDisplayName() val notifiableEvent = NotifiableMessageEvent( eventId = event.root.eventId!!, @@ -126,7 +126,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St val body = displayableEventFormatter.format(event, false).toString() val roomName = room.roomSummary()?.displayName ?: "" - val senderDisplayName = event.getDisambiguatedDisplayName() + val senderDisplayName = event.senderInfo.getDisambiguatedDisplayName() val notifiableEvent = NotifiableMessageEvent( eventId = event.root.eventId!!, @@ -151,7 +151,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St ContentUrlResolver.ThumbnailMethod.SCALE) notifiableEvent.senderAvatarPath = session.contentUrlResolver() - .resolveThumbnail(event.senderAvatar, + .resolveThumbnail(event.senderInfo.avatarUrl, 250, 250, ContentUrlResolver.ThumbnailMethod.SCALE) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt index 918fc433c9..8c4dd1f300 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt @@ -109,7 +109,7 @@ class UploadsFileController @Inject constructor( id(uploadEvent.eventId) title(uploadEvent.contentWithAttachmentContent.body) subtitle(stringProvider.getString(R.string.uploads_files_subtitle, - uploadEvent.uploadSenderInfo.getDisambiguatedDisplayName(), + uploadEvent.senderInfo.getDisambiguatedDisplayName(), dateFormatter.formatRelativeDateTime(uploadEvent.root.originServerTs))) listener(object : UploadsFileItem.Listener { override fun onItemClicked() { From a95102a78f7a59e5604524f2fdadc5dbf0de6a76 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 May 2020 13:24:42 +0200 Subject: [PATCH 14/17] Uploads: Use StateView for better Loading/Empty rendering --- .../vector/riotx/core/platform/StateView.kt | 29 +++------- .../uploads/files/RoomUploadsFilesFragment.kt | 49 ++++++++++++++--- .../uploads/files/UploadsFileController.kt | 55 +++---------------- .../uploads/media/RoomUploadsMediaFragment.kt | 53 ++++++++++++++---- .../uploads/media/UploadsMediaController.kt | 54 +++--------------- .../fragment_generic_state_view_recycler.xml | 13 +++++ vector/src/main/res/layout/view_state.xml | 1 + vector/src/main/res/values/strings.xml | 4 +- 8 files changed, 125 insertions(+), 133 deletions(-) create mode 100644 vector/src/main/res/layout/fragment_generic_state_view_recycler.xml diff --git a/vector/src/main/java/im/vector/riotx/core/platform/StateView.kt b/vector/src/main/java/im/vector/riotx/core/platform/StateView.kt index 4c5a987b4b..bc24874f9f 100755 --- a/vector/src/main/java/im/vector/riotx/core/platform/StateView.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/StateView.kt @@ -21,6 +21,7 @@ import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.View import android.widget.FrameLayout +import androidx.core.view.isVisible import im.vector.riotx.R import kotlinx.android.synthetic.main.view_state.view.* @@ -31,6 +32,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? object Content : State() object Loading : State() data class Empty(val title: CharSequence? = null, val image: Drawable? = null, val message: CharSequence? = null) : State() + data class Error(val message: CharSequence? = null) : State() } @@ -59,34 +61,21 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? } private fun update(newState: State) { + progressBar.isVisible = newState is State.Loading + errorView.isVisible = newState is State.Error + emptyView.isVisible = newState is State.Empty + contentView?.isVisible = newState is State.Content + when (newState) { - is State.Content -> { - progressBar.visibility = View.INVISIBLE - errorView.visibility = View.INVISIBLE - emptyView.visibility = View.INVISIBLE - contentView?.visibility = View.VISIBLE - } - is State.Loading -> { - progressBar.visibility = View.VISIBLE - errorView.visibility = View.INVISIBLE - emptyView.visibility = View.INVISIBLE - contentView?.visibility = View.INVISIBLE - } + is State.Content -> Unit + is State.Loading -> Unit is State.Empty -> { - progressBar.visibility = View.INVISIBLE - errorView.visibility = View.INVISIBLE - emptyView.visibility = View.VISIBLE emptyImageView.setImageDrawable(newState.image) emptyMessageView.text = newState.message emptyTitleView.text = newState.title - contentView?.visibility = View.INVISIBLE } is State.Error -> { - progressBar.visibility = View.INVISIBLE - errorView.visibility = View.VISIBLE - emptyView.visibility = View.INVISIBLE errorMessageView.text = newState.message - contentView?.visibility = View.INVISIBLE } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt index 63f9e5215e..bba7a40440 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt @@ -18,6 +18,10 @@ package im.vector.riotx.features.roomprofile.uploads.files import android.os.Bundle import android.view.View +import androidx.core.content.ContextCompat +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState import im.vector.matrix.android.api.session.room.uploads.UploadEvent @@ -25,31 +29,37 @@ import im.vector.riotx.R import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.trackItemsVisibilityChange +import im.vector.riotx.core.platform.StateView import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.features.roomprofile.uploads.RoomUploadsAction import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel -import kotlinx.android.synthetic.main.fragment_generic_recycler.* +import kotlinx.android.synthetic.main.fragment_generic_state_view_recycler.* import javax.inject.Inject class RoomUploadsFilesFragment @Inject constructor( private val controller: UploadsFileController -) : VectorBaseFragment(), UploadsFileController.Listener { +) : VectorBaseFragment(), + UploadsFileController.Listener, + StateView.EventCallback { private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class) - override fun getLayoutResId() = R.layout.fragment_generic_recycler + override fun getLayoutResId() = R.layout.fragment_generic_state_view_recycler override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - recyclerView.trackItemsVisibilityChange() - recyclerView.configureWith(controller, showDivider = true) + genericStateViewListStateView.contentView = genericStateViewListRecycler + genericStateViewListStateView.eventCallback = this + + genericStateViewListRecycler.trackItemsVisibilityChange() + genericStateViewListRecycler.configureWith(controller, showDivider = true) controller.listener = this } override fun onDestroyView() { super.onDestroyView() - recyclerView.cleanup() + genericStateViewListRecycler.cleanup() controller.listener = null } @@ -58,7 +68,7 @@ class RoomUploadsFilesFragment @Inject constructor( uploadsViewModel.handle(RoomUploadsAction.Share(uploadEvent)) } - override fun onRetry() { + override fun onRetryClicked() { uploadsViewModel.handle(RoomUploadsAction.Retry) } @@ -75,6 +85,29 @@ class RoomUploadsFilesFragment @Inject constructor( } override fun invalidate() = withState(uploadsViewModel) { state -> - controller.setData(state) + if (state.fileEvents.isEmpty()) { + when (state.asyncEventsRequest) { + is Loading -> { + genericStateViewListStateView.state = StateView.State.Loading + } + is Fail -> { + genericStateViewListStateView.state = StateView.State.Error(errorFormatter.toHumanReadable(state.asyncEventsRequest.error)) + } + is Success -> { + if (state.hasMore) { + // We need to load more items + loadMore() + } else { + genericStateViewListStateView.state = StateView.State.Empty( + title = getString(R.string.uploads_files_no_result), + image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_file) + ) + } + } + } + } else { + genericStateViewListStateView.state = StateView.State.Content + controller.setData(state) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt index 8c4dd1f300..2b355af8a9 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt @@ -18,28 +18,20 @@ package im.vector.riotx.features.roomprofile.uploads.files import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.VisibilityState -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success import im.vector.matrix.android.api.session.room.uploads.UploadEvent import im.vector.riotx.R import im.vector.riotx.core.date.VectorDateFormatter -import im.vector.riotx.core.epoxy.errorWithRetryItem import im.vector.riotx.core.epoxy.loadingItem -import im.vector.riotx.core.epoxy.noResultItem -import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewState import javax.inject.Inject class UploadsFileController @Inject constructor( - private val errorFormatter: ErrorFormatter, private val stringProvider: StringProvider, private val dateFormatter: VectorDateFormatter ) : TypedEpoxyController() { interface Listener { - fun onRetry() fun loadMore() fun onOpenClicked(uploadEvent: UploadEvent) fun onDownloadClicked(uploadEvent: UploadEvent) @@ -57,46 +49,15 @@ class UploadsFileController @Inject constructor( override fun buildModels(data: RoomUploadsViewState?) { data ?: return - if (data.fileEvents.isEmpty()) { - when (data.asyncEventsRequest) { - is Loading -> { - loadingItem { - id("loading") - } - } - is Fail -> { - errorWithRetryItem { - id("error") - text(errorFormatter.toHumanReadable(data.asyncEventsRequest.error)) - listener { listener?.onRetry() } - } - } - is Success -> { - if (data.hasMore) { - // We need to load more items - listener?.loadMore() - loadingItem { - id("loading") - } - } else { - noResultItem { - id("noResult") - text(stringProvider.getString(R.string.uploads_files_no_result)) - } - } - } - } - } else { - buildFileItems(data.fileEvents) + buildFileItems(data.fileEvents) - if (data.hasMore) { - loadingItem { - // Always use a different id, because we can be notified several times of visibility state changed - id("loadMore${idx++}") - onVisibilityStateChanged { _, _, visibilityState -> - if (visibilityState == VisibilityState.VISIBLE) { - listener?.loadMore() - } + if (data.hasMore) { + loadingItem { + // Always use a different id, because we can be notified several times of visibility state changed + id("loadMore${idx++}") + onVisibilityStateChanged { _, _, visibilityState -> + if (visibilityState == VisibilityState.VISIBLE) { + listener?.loadMore() } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt index a722c8281f..a4e6c61238 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt @@ -19,37 +19,47 @@ package im.vector.riotx.features.roomprofile.uploads.media import android.os.Bundle import android.util.DisplayMetrics import android.view.View +import androidx.core.content.ContextCompat import androidx.recyclerview.widget.GridLayoutManager +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.extensions.trackItemsVisibilityChange +import im.vector.riotx.core.platform.StateView import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.roomprofile.uploads.RoomUploadsAction import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel -import kotlinx.android.synthetic.main.fragment_generic_recycler.* +import kotlinx.android.synthetic.main.fragment_generic_state_view_recycler.* import javax.inject.Inject class RoomUploadsMediaFragment @Inject constructor( private val controller: UploadsMediaController, private val dimensionConverter: DimensionConverter -) : VectorBaseFragment(), UploadsMediaController.Listener { +) : VectorBaseFragment(), + UploadsMediaController.Listener, + StateView.EventCallback { private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class) - override fun getLayoutResId() = R.layout.fragment_generic_recycler + override fun getLayoutResId() = R.layout.fragment_generic_state_view_recycler override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - recyclerView.trackItemsVisibilityChange() - recyclerView.layoutManager = GridLayoutManager(context, getNumberOfColumns()) - recyclerView.adapter = controller.adapter - recyclerView.setHasFixedSize(true) + genericStateViewListStateView.contentView = genericStateViewListRecycler + genericStateViewListStateView.eventCallback = this + + genericStateViewListRecycler.trackItemsVisibilityChange() + genericStateViewListRecycler.layoutManager = GridLayoutManager(context, getNumberOfColumns()) + genericStateViewListRecycler.adapter = controller.adapter + genericStateViewListRecycler.setHasFixedSize(true) controller.listener = this } @@ -62,7 +72,7 @@ class RoomUploadsMediaFragment @Inject constructor( override fun onDestroyView() { super.onDestroyView() - recyclerView.cleanup() + genericStateViewListRecycler.cleanup() controller.listener = null } @@ -78,11 +88,34 @@ class RoomUploadsMediaFragment @Inject constructor( uploadsViewModel.handle(RoomUploadsAction.LoadMore) } - override fun onRetry() { + override fun onRetryClicked() { uploadsViewModel.handle(RoomUploadsAction.Retry) } override fun invalidate() = withState(uploadsViewModel) { state -> - controller.setData(state) + if (state.mediaEvents.isEmpty()) { + when (state.asyncEventsRequest) { + is Loading -> { + genericStateViewListStateView.state = StateView.State.Loading + } + is Fail -> { + genericStateViewListStateView.state = StateView.State.Error(errorFormatter.toHumanReadable(state.asyncEventsRequest.error)) + } + is Success -> { + if (state.hasMore) { + // We need to load more items + loadMore() + } else { + genericStateViewListStateView.state = StateView.State.Empty( + title = getString(R.string.uploads_media_no_result), + image = ContextCompat.getDrawable(requireContext(), R.drawable.ic_image) + ) + } + } + } + } else { + genericStateViewListStateView.state = StateView.State.Content + controller.setData(state) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt index 1d797bec78..cd3e401dc5 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt @@ -19,18 +19,12 @@ package im.vector.riotx.features.roomprofile.uploads.media import android.view.View import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.VisibilityState -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.Success import im.vector.matrix.android.api.session.room.model.message.MessageImageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.uploads.UploadEvent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt -import im.vector.riotx.R -import im.vector.riotx.core.epoxy.errorWithRetryItem -import im.vector.riotx.core.epoxy.noResultItem import im.vector.riotx.core.epoxy.squareLoadingItem import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.resources.StringProvider @@ -48,7 +42,6 @@ class UploadsMediaController @Inject constructor( ) : TypedEpoxyController() { interface Listener { - fun onRetry() fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) fun loadMore() @@ -67,46 +60,15 @@ class UploadsMediaController @Inject constructor( override fun buildModels(data: RoomUploadsViewState?) { data ?: return - if (data.mediaEvents.isEmpty()) { - when (data.asyncEventsRequest) { - is Loading -> { - squareLoadingItem { - id("loading") - } - } - is Fail -> { - errorWithRetryItem { - id("error") - text(errorFormatter.toHumanReadable(data.asyncEventsRequest.error)) - listener { listener?.onRetry() } - } - } - is Success -> { - if (data.hasMore) { - // We need to load more items - listener?.loadMore() - squareLoadingItem { - id("loading") - } - } else { - noResultItem { - id("noResult") - text(stringProvider.getString(R.string.uploads_media_no_result)) - } - } - } - } - } else { - buildMediaItems(data.mediaEvents) + buildMediaItems(data.mediaEvents) - if (data.hasMore) { - squareLoadingItem { - // Always use a different id, because we can be notified several times of visibility state changed - id("loadMore${idx++}") - onVisibilityStateChanged { _, _, visibilityState -> - if (visibilityState == VisibilityState.VISIBLE) { - listener?.loadMore() - } + if (data.hasMore) { + squareLoadingItem { + // Always use a different id, because we can be notified several times of visibility state changed + id("loadMore${idx++}") + onVisibilityStateChanged { _, _, visibilityState -> + if (visibilityState == VisibilityState.VISIBLE) { + listener?.loadMore() } } } diff --git a/vector/src/main/res/layout/fragment_generic_state_view_recycler.xml b/vector/src/main/res/layout/fragment_generic_state_view_recycler.xml new file mode 100644 index 0000000000..410373b97f --- /dev/null +++ b/vector/src/main/res/layout/fragment_generic_state_view_recycler.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/vector/src/main/res/layout/view_state.xml b/vector/src/main/res/layout/view_state.xml index c17e1b216b..082a0bb24c 100644 --- a/vector/src/main/res/layout/view_state.xml +++ b/vector/src/main/res/layout/view_state.xml @@ -73,6 +73,7 @@ android:layout_width="64dp" android:layout_height="64dp" android:layout_gravity="center_horizontal" + android:tint="?riotx_text_primary" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index b0ddce5ed3..36532f25e3 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1789,11 +1789,11 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Couldn\'t handle share data MEDIA - There is no media in this room + There are no media in this room FILES %1$s at %2$s - There is no files in this room + There are no files in this room "It's spam" "It's inappropriate" From f0f3e8ddb9e5ba46f6eda056e62a5a3a2a02d97a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 May 2020 14:27:10 +0200 Subject: [PATCH 15/17] Uploads: auto-review --- .../api/session/room/sender/SenderInfo.kt | 9 ++++---- .../session/room/uploads/GetUploadsResult.kt | 3 +-- .../matrix/android/api/util/MatrixItem.kt | 2 +- .../membership/RoomDisplayNameResolver.kt | 2 +- .../room/send/LocalEchoEventFactory.kt | 2 +- .../session/room/uploads/GetUploadsTask.kt | 3 +-- vector/build.gradle | 3 ++- .../home/room/detail/RoomDetailFragment.kt | 2 +- .../factory/MergedHeaderItemFactory.kt | 4 ++-- .../format/DisplayableEventFormatter.kt | 2 +- .../timeline/format/NoticeEventFormatter.kt | 22 +++++++++---------- .../helper/MessageInformationDataFactory.kt | 4 ++-- .../reactions/ViewReactionsViewModel.kt | 2 +- .../notifications/NotifiableEventResolver.kt | 4 ++-- .../uploads/RoomUploadsFragment.kt | 8 ------- .../uploads/RoomUploadsViewModel.kt | 9 ++++---- .../uploads/RoomUploadsViewState.kt | 1 - .../uploads/files/UploadsFileController.kt | 2 +- 18 files changed, 36 insertions(+), 48 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/sender/SenderInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/sender/SenderInfo.kt index c3f0df8312..1a0908a6d3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/sender/SenderInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/sender/SenderInfo.kt @@ -19,17 +19,16 @@ package im.vector.matrix.android.api.session.room.sender data class SenderInfo( val userId: String, /** - * Consider using [getDisambiguatedDisplayName] + * Consider using [disambiguatedDisplayName] */ val displayName: String?, val isUniqueDisplayName: Boolean, val avatarUrl: String? ) { - fun getDisambiguatedDisplayName(): String { - return when { + val disambiguatedDisplayName: String + get() = when { displayName.isNullOrBlank() -> userId isUniqueDisplayName -> displayName - else -> "$displayName (${userId})" + else -> "$displayName ($userId)" } - } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt index bbe8641edd..4c75d909aa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/GetUploadsResult.kt @@ -18,10 +18,9 @@ package im.vector.matrix.android.api.session.room.uploads data class GetUploadsResult( // List of fetched Events, most recent first - val events: List, + val uploadEvents: List, // token to get more events val nextToken: String, // True if there are more event to load val hasMore: Boolean ) - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt index 95c0435f1b..f30494711b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt @@ -156,4 +156,4 @@ fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAl fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) -fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, getDisambiguatedDisplayName(), avatarUrl) +fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt index e7a68b3b5b..3c1df50b75 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -126,7 +126,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: return name ?: roomId } - /** See [im.vector.matrix.android.api.session.room.timeline.TimelineEvent.getDisambiguatedDisplayName] */ + /** See [im.vector.matrix.android.api.session.room.sender.SenderInfo.disambiguatedDisplayName] */ private fun resolveRoomMemberName(roomMemberSummary: RoomMemberSummaryEntity?, roomMemberHelper: RoomMemberHelper): String? { if (roomMemberSummary == null) return null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 2d1e2775c0..82d393e79a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -202,7 +202,7 @@ internal class LocalEchoEventFactory @Inject constructor( permalink, stringProvider.getString(R.string.message_reply_to_prefix), userLink, - originalEvent.senderInfo.getDisambiguatedDisplayName(), + originalEvent.senderInfo.disambiguatedDisplayName, body.takeFormatted(), createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted() ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt index cc78a02c88..fa707c0bf8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt @@ -91,9 +91,8 @@ internal class DefaultGetUploadsTask @Inject constructor( } } - return GetUploadsResult( - events = uploadEvents, + uploadEvents = uploadEvents, nextToken = chunk.end ?: "", hasMore = chunk.hasMore() ) diff --git a/vector/build.gradle b/vector/build.gradle index b9a7fbcdc8..6f1afc5038 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -260,6 +260,7 @@ dependencies { def autofill_version = "1.0.0" def work_version = '2.3.3' def arch_version = '2.1.0' + def lifecycle_version = '2.2.0' implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android-rx") @@ -282,7 +283,7 @@ dependencies { implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0" implementation "com.squareup.moshi:moshi-adapters:$moshi_version" - implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" // Log diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 9dcf0d42ea..f042cdcefb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -458,7 +458,7 @@ class RoomDetailFragment @Inject constructor( autoCompleter.enterSpecialMode() // switch to expanded bar composerLayout.composerRelatedMessageTitle.apply { - text = event.senderInfo.getDisambiguatedDisplayName() + text = event.senderInfo.disambiguatedDisplayName setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.senderId))) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 70d70c2765..419fd673d1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -88,7 +88,7 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av val data = BasedMergedItem.Data( userId = mergedEvent.root.senderId ?: "", avatarUrl = mergedEvent.senderInfo.avatarUrl, - memberName = mergedEvent.senderInfo.getDisambiguatedDisplayName(), + memberName = mergedEvent.senderInfo.disambiguatedDisplayName, localId = mergedEvent.localId, eventId = mergedEvent.root.eventId ?: "" ) @@ -159,7 +159,7 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av val data = BasedMergedItem.Data( userId = mergedEvent.root.senderId ?: "", avatarUrl = mergedEvent.senderInfo.avatarUrl, - memberName = mergedEvent.senderInfo.getDisambiguatedDisplayName(), + memberName = mergedEvent.senderInfo.disambiguatedDisplayName, localId = mergedEvent.localId, eventId = mergedEvent.root.eventId ?: "" ) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index ac587a8250..9ab48ad5ee 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -45,7 +45,7 @@ class DisplayableEventFormatter @Inject constructor( return stringProvider.getString(R.string.encrypted_message) } - val senderName = timelineEvent.senderInfo.getDisambiguatedDisplayName() + val senderName = timelineEvent.senderInfo.disambiguatedDisplayName when (timelineEvent.root.getClearType()) { EventType.MESSAGE -> { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 99fa25e76f..86c9f0ab5b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -47,20 +47,20 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active fun format(timelineEvent: TimelineEvent): CharSequence? { return when (val type = timelineEvent.root.getClearType()) { - EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root) - EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) - EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.senderInfo.getDisambiguatedDisplayName()) + EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) + EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) + EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) + EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) + EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) + EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) + EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) + EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) + EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.senderInfo.disambiguatedDisplayName) EventType.CALL_INVITE, EventType.CALL_HANGUP, - EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.senderInfo.getDisambiguatedDisplayName()) + EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName) EventType.MESSAGE, EventType.REACTION, EventType.KEY_VERIFICATION_START, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 92374edea7..9a912b5af3 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -65,13 +65,13 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses val showInformation = addDaySeparator || event.senderInfo.avatarUrl != nextEvent?.senderInfo?.avatarUrl - || event.senderInfo.getDisambiguatedDisplayName() != nextEvent?.senderInfo?.getDisambiguatedDisplayName() + || event.senderInfo.disambiguatedDisplayName != nextEvent?.senderInfo?.disambiguatedDisplayName || (nextEvent.root.getClearType() != EventType.MESSAGE && nextEvent.root.getClearType() != EventType.ENCRYPTED) || isNextMessageReceivedMoreThanOneHourAgo || isTileTypeMessage(nextEvent) val time = dateFormatter.formatMessageHour(date) - val formattedMemberName = span(event.senderInfo.getDisambiguatedDisplayName()) { + val formattedMemberName = span(event.senderInfo.disambiguatedDisplayName) { textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId)) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt index 25e407eeb1..3d8382ab98 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsViewModel.kt @@ -111,7 +111,7 @@ class ViewReactionsViewModel @AssistedInject constructor(@Assisted event.root.eventId!!, summary.key, event.root.senderId ?: "", - event.senderInfo.getDisambiguatedDisplayName(), + event.senderInfo.disambiguatedDisplayName, dateFormatter.formatRelativeDateTime(event.root.originServerTs) ) diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt index 9401c69ad1..a2dc8d33f0 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt @@ -93,7 +93,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St // Ok room is not known in store, but we can still display something val body = displayableEventFormatter.format(event, false) val roomName = stringProvider.getString(R.string.notification_unknown_room_name) - val senderDisplayName = event.senderInfo.getDisambiguatedDisplayName() + val senderDisplayName = event.senderInfo.disambiguatedDisplayName val notifiableEvent = NotifiableMessageEvent( eventId = event.root.eventId!!, @@ -126,7 +126,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St val body = displayableEventFormatter.format(event, false).toString() val roomName = room.roomSummary()?.displayName ?: "" - val senderDisplayName = event.senderInfo.getDisambiguatedDisplayName() + val senderDisplayName = event.senderInfo.disambiguatedDisplayName val notifiableEvent = NotifiableMessageEvent( eventId = event.root.eventId!!, diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt index 9ef2688b17..cf20cca834 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt @@ -87,14 +87,6 @@ class RoomUploadsFragment @Inject constructor( } } - /* - override fun onDestroyView() { - super.onDestroyView() - // Clear your view, unsubscribe... - } - - */ - override fun invalidate() = withState(viewModel) { state -> renderRoomSummary(state) } diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt index dcb0963ef0..952e80c035 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt @@ -96,7 +96,7 @@ class RoomUploadsViewModel @AssistedInject constructor( token = result.nextToken - val groupedEvents = result.events + val groupedUploadEvents = result.uploadEvents .groupBy { it.contentWithAttachmentContent.msgType == MessageType.MSGTYPE_IMAGE || it.contentWithAttachmentContent.msgType == MessageType.MSGTYPE_VIDEO @@ -105,13 +105,13 @@ class RoomUploadsViewModel @AssistedInject constructor( setState { copy( asyncEventsRequest = Success(Unit), - mediaEvents = this.mediaEvents + groupedEvents[true].orEmpty(), - fileEvents = this.fileEvents + groupedEvents[false].orEmpty(), + mediaEvents = this.mediaEvents + groupedUploadEvents[true].orEmpty(), + fileEvents = this.fileEvents + groupedUploadEvents[false].orEmpty(), hasMore = result.hasMore ) } } catch (failure: Throwable) { - // TODO Post fail + _viewEvents.post(RoomUploadsViewEvents.Failure(failure)) setState { copy( asyncEventsRequest = Fail(failure) @@ -163,7 +163,6 @@ class RoomUploadsViewModel @AssistedInject constructor( action.uploadEvent.contentWithAttachmentContent.getFileUrl(), action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(), it) - } _viewEvents.post(RoomUploadsViewEvents.FileReadyForSaving(file, action.uploadEvent.contentWithAttachmentContent.body)) } catch (failure: Throwable) { diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewState.kt index bed3b264cd..3e31a3cdd6 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewState.kt @@ -37,4 +37,3 @@ data class RoomUploadsViewState( constructor(args: RoomProfileArgs) : this(roomId = args.roomId) } - diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt index 2b355af8a9..60f966e7d2 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt @@ -70,7 +70,7 @@ class UploadsFileController @Inject constructor( id(uploadEvent.eventId) title(uploadEvent.contentWithAttachmentContent.body) subtitle(stringProvider.getString(R.string.uploads_files_subtitle, - uploadEvent.senderInfo.getDisambiguatedDisplayName(), + uploadEvent.senderInfo.disambiguatedDisplayName, dateFormatter.formatRelativeDateTime(uploadEvent.root.originServerTs))) listener(object : UploadsFileItem.Listener { override fun onItemClicked() { From c52aae7c2986d479d2e0003cf7c141b9c962577f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 May 2020 14:49:01 +0200 Subject: [PATCH 16/17] Uploads: Snackbar --- .../features/roomprofile/uploads/RoomUploadsFragment.kt | 6 +++--- vector/src/main/res/layout/fragment_room_uploads.xml | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt index cf20cca834..99aeb4231b 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt @@ -18,11 +18,11 @@ package im.vector.riotx.features.roomprofile.uploads import android.os.Bundle import android.view.View -import android.widget.Toast import androidx.core.net.toUri import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayoutMediator import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R @@ -77,9 +77,9 @@ class RoomUploadsFragment @Inject constructor( mediaMimeType = getMimeTypeFromUri(requireContext(), it.file.toUri()) ) if (saved) { - Toast.makeText(requireContext(), R.string.media_file_added_to_gallery, Toast.LENGTH_LONG).show() + Snackbar.make(roomUploadsCoordinator, R.string.media_file_added_to_gallery, Snackbar.LENGTH_LONG).show() } else { - Toast.makeText(requireContext(), R.string.error_adding_media_file_to_gallery, Toast.LENGTH_LONG).show() + Snackbar.make(roomUploadsCoordinator, R.string.error_adding_media_file_to_gallery, Snackbar.LENGTH_LONG).show() } } is RoomUploadsViewEvents.Failure -> showFailure(it.throwable) diff --git a/vector/src/main/res/layout/fragment_room_uploads.xml b/vector/src/main/res/layout/fragment_room_uploads.xml index 36d779b533..5e289d4724 100644 --- a/vector/src/main/res/layout/fragment_room_uploads.xml +++ b/vector/src/main/res/layout/fragment_room_uploads.xml @@ -2,6 +2,7 @@ From 7a3dbecc081da2971079ef07787ed1da3adb097f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 20 May 2020 14:52:28 +0200 Subject: [PATCH 17/17] Fixes #860 --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index d88f9952e4..2dc0cb3ea0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ Changes in RiotX 0.21.0 (2020-XX-XX) Features ✨: - Identity server support (#607) - Switch language support (#41) + - Display list of attachments of a room (#860) Improvements 🙌: - Better connectivity lost indicator when airplane mode is on