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 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..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 @@ -220,3 +220,11 @@ fun Event.isImageMessage(): Boolean { else -> false } } + +fun Event.isVideoMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel()?.msgType) { + MessageType.MSGTYPE_VIDEO -> true + else -> false + } +} 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/sender/SenderInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/sender/SenderInfo.kt new file mode 100644 index 0000000000..1a0908a6d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/sender/SenderInfo.kt @@ -0,0 +1,34 @@ +/* + * 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.sender + +data class SenderInfo( + val userId: String, + /** + * Consider using [disambiguatedDisplayName] + */ + val displayName: String?, + val isUniqueDisplayName: Boolean, + val avatarUrl: String? +) { + val disambiguatedDisplayName: String + get() = when { + 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/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..4c75d909aa --- /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 + +data class GetUploadsResult( + // List of fetched Events, most recent first + 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/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..5df2b9c9e0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/uploads/UploadEvent.kt @@ -0,0 +1,32 @@ +/* + * 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 +import im.vector.matrix.android.api.session.room.sender.SenderInfo + +/** + * 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 senderInfo: SenderInfo +) 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/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt index d5aa897c7d..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 @@ -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, disambiguatedDisplayName, 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/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..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,12 +20,21 @@ import im.vector.matrix.android.api.session.events.model.EventType internal object FilterFactory { - fun createDefaultFilterBody(): FilterBody { - return FilterUtil.enableLazyLoading(FilterBody(), true) + fun createUploadsFilter(numberOfEvents: Int): RoomEventFilter { + return RoomEventFilter( + limit = numberOfEvents, + containsUrl = true, + types = listOf(EventType.MESSAGE), + lazyLoadMembers = true + ) } - fun createRiotFilterBody(): FilterBody { - return FilterBody( + fun createDefaultFilter(): Filter { + return FilterUtil.enableLazyLoading(Filter(), true) + } + + 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..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 @@ -25,14 +25,46 @@ import im.vector.matrix.android.internal.di.MoshiProvider */ @JsonClass(generateAdapter = true) data class RoomEventFilter( - @Json(name = "limit") var limit: Int? = null, + /** + * The maximum number of events to return. + */ + @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. + */ @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 ) { 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/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt index feb05a3275..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,6 +126,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: return name ?: roomId } + /** 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 2a24094b5d..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.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/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/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..dd8269a079 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/DefaultUploadsService.kt @@ -0,0 +1,46 @@ +/* + * 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)) { + 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 new file mode 100644 index 0000000000..fa707c0bf8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/uploads/GetUploadsTask.kt @@ -0,0 +1,100 @@ +/* + * 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.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.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 +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 monarchy: Monarchy, + 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) + } + + var uploadEvents = listOf() + + 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) + SenderInfo( + userId = senderId, + displayName = roomMemberSummaryEntity?.displayName, + isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(roomMemberSummaryEntity?.displayName), + avatarUrl = roomMemberSummaryEntity?.avatarUrl + ) + } + + UploadEvent( + root = event, + eventId = eventId, + contentWithAttachmentContent = messageWithAttachmentContent, + senderInfo = senderInfo + ) + } + } + + return GetUploadsResult( + uploadEvents = uploadEvents, + nextToken = chunk.end ?: "", + hasMore = chunk.hasMore() + ) + } +} diff --git a/vector/build.gradle b/vector/build.gradle index 74fc96a425..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,6 +283,7 @@ dependencies { implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0" implementation "com.squareup.moshi:moshi-adapters:$moshi_version" + 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/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index bc526f93a5..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 @@ -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.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 @@ -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/core/epoxy/SquareLoadingItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/SquareLoadingItem.kt new file mode 100644 index 0000000000..c0f6eb198f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/SquareLoadingItem.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.riotx.core.epoxy + +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R + +@EpoxyModelClass(layout = R.layout.item_loading_square) +abstract class SquareLoadingItem : VectorEpoxyModel() { + + class Holder : VectorEpoxyHolder() +} 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/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/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 826d9a495a..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, @@ -36,4 +39,8 @@ class DimensionConverter @Inject constructor(val resources: Resources) { resources.displayMetrics ).toInt() } + + fun pxToDp(@Px px: Int): Int { + return (px.toFloat() / resources.displayMetrics.density).toInt() + } } 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/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 079a2927d7..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 @@ -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 @@ -49,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 @@ -95,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 @@ -150,9 +147,7 @@ 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 import im.vector.riotx.features.permalink.NavigationInterceptor import im.vector.riotx.features.permalink.PermalinkHandler @@ -463,7 +458,7 @@ class RoomDetailFragment @Inject constructor( autoCompleter.enterSpecialMode() // switch to expanded bar composerLayout.composerRelatedMessageTitle.apply { - text = event.getDisambiguatedDisplayName() + text = event.senderInfo.disambiguatedDisplayName setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.senderId))) } @@ -482,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) { @@ -543,8 +534,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) @@ -998,31 +988,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/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..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 @@ -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.disambiguatedDisplayName, 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.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 2f7b52de62..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.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 f29bd72e0a..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.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.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.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.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 695da73f89..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 @@ -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.disambiguatedDisplayName != nextEvent?.senderInfo?.disambiguatedDisplayName || (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.disambiguatedDisplayName) { 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..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.getDisambiguatedDisplayName(), + event.senderInfo.disambiguatedDisplayName, dateFormatter.formatRelativeDateTime(event.root.originServerTs) ) 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..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 @@ -65,6 +66,18 @@ 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)) + .placeholder(R.drawable.ic_image) + .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/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotifiableEventResolver.kt index 1f9f54127b..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.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.getDisambiguatedDisplayName() + val senderDisplayName = event.senderInfo.disambiguatedDisplayName 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/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/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..59571de122 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsAction.kt @@ -0,0 +1,28 @@ +/* + * 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.matrix.android.api.session.room.uploads.UploadEvent +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class RoomUploadsAction : VectorViewModelAction { + 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/RoomUploadsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt new file mode 100644 index 0000000000..99aeb4231b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsFragment.kt @@ -0,0 +1,100 @@ +/* + * 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 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 +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.* +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(this) + roomUploadsViewPager.adapter = sectionsPagerAdapter + + 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) + + 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) { + Snackbar.make(roomUploadsCoordinator, R.string.media_file_added_to_gallery, Snackbar.LENGTH_LONG).show() + } else { + Snackbar.make(roomUploadsCoordinator, R.string.error_adding_media_file_to_gallery, Snackbar.LENGTH_LONG).show() + } + } + is RoomUploadsViewEvents.Failure -> showFailure(it.throwable) + }.exhaustive + } + } + + override fun invalidate() = withState(viewModel) { state -> + renderRoomSummary(state) + } + + private fun renderRoomSummary(state: RoomUploadsViewState) { + state.roomSummary()?.let { + 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 new file mode 100644 index 0000000000..6866d5e2fa --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsPagerAdapter.kt @@ -0,0 +1,37 @@ +/* + * 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.viewpager2.adapter.FragmentStateAdapter +import im.vector.riotx.features.roomprofile.uploads.files.RoomUploadsFilesFragment +import im.vector.riotx.features.roomprofile.uploads.media.RoomUploadsMediaFragment + +class RoomUploadsPagerAdapter( + private val fragment: Fragment +) : FragmentStateAdapter(fragment) { + + override fun getItemCount() = 2 + + override fun createFragment(position: Int): Fragment { + return if (position == 0) { + fragment.childFragmentManager.fragmentFactory.instantiate(fragment.requireContext().classLoader, RoomUploadsMediaFragment::class.java.name) + } else { + fragment.childFragmentManager.fragmentFactory.instantiate(fragment.requireContext().classLoader, RoomUploadsFilesFragment::class.java.name) + } + } +} 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 new file mode 100644 index 0000000000..952e80c035 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewModel.kt @@ -0,0 +1,173 @@ +/* + * 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.Success +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.file.FileService +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 +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.VectorViewModel +import kotlinx.coroutines.launch +import java.io.File + +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 + if (!state.hasMore) return@withState + + setState { + copy( + asyncEventsRequest = Loading() + ) + } + + viewModelScope.launch { + try { + val result = awaitCallback { + room.getUploads(20, token, it) + } + + token = result.nextToken + + val groupedUploadEvents = result.uploadEvents + .groupBy { + it.contentWithAttachmentContent.msgType == MessageType.MSGTYPE_IMAGE + || it.contentWithAttachmentContent.msgType == MessageType.MSGTYPE_VIDEO + } + + setState { + copy( + asyncEventsRequest = Success(Unit), + mediaEvents = this.mediaEvents + groupedUploadEvents[true].orEmpty(), + fileEvents = this.fileEvents + groupedUploadEvents[false].orEmpty(), + hasMore = result.hasMore + ) + } + } catch (failure: Throwable) { + _viewEvents.post(RoomUploadsViewEvents.Failure(failure)) + setState { + copy( + asyncEventsRequest = Fail(failure) + ) + } + } + } + } + + private var token: String? = null + + override fun handle(action: RoomUploadsAction) { + when (action) { + 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.uploadEvent.eventId, + action.uploadEvent.contentWithAttachmentContent.body, + action.uploadEvent.contentWithAttachmentContent.getFileUrl(), + action.uploadEvent.contentWithAttachmentContent.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.uploadEvent.eventId, + action.uploadEvent.contentWithAttachmentContent.body, + action.uploadEvent.contentWithAttachmentContent.getFileUrl(), + action.uploadEvent.contentWithAttachmentContent.encryptedFileInfo?.toElementToDecrypt(), + it) + } + _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 new file mode 100644 index 0000000000..3e31a3cdd6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/RoomUploadsViewState.kt @@ -0,0 +1,39 @@ +/* + * 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.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(), + // Current pagination request + val asyncEventsRequest: Async = Uninitialized, + // True if more result are available server side + 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 new file mode 100644 index 0000000000..bba7a40440 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/RoomUploadsFilesFragment.kt @@ -0,0 +1,113 @@ +/* + * 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 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 +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_state_view_recycler.* +import javax.inject.Inject + +class RoomUploadsFilesFragment @Inject constructor( + private val controller: UploadsFileController +) : VectorBaseFragment(), + UploadsFileController.Listener, + StateView.EventCallback { + + private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class) + + override fun getLayoutResId() = R.layout.fragment_generic_state_view_recycler + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + genericStateViewListStateView.contentView = genericStateViewListRecycler + genericStateViewListStateView.eventCallback = this + + genericStateViewListRecycler.trackItemsVisibilityChange() + genericStateViewListRecycler.configureWith(controller, showDivider = true) + controller.listener = this + } + + override fun onDestroyView() { + super.onDestroyView() + genericStateViewListRecycler.cleanup() + controller.listener = null + } + + override fun onOpenClicked(uploadEvent: UploadEvent) { + // Same action than Share + uploadsViewModel.handle(RoomUploadsAction.Share(uploadEvent)) + } + + override fun onRetryClicked() { + uploadsViewModel.handle(RoomUploadsAction.Retry) + } + + override fun loadMore() { + uploadsViewModel.handle(RoomUploadsAction.LoadMore) + } + + override fun onDownloadClicked(uploadEvent: UploadEvent) { + uploadsViewModel.handle(RoomUploadsAction.Download(uploadEvent)) + } + + override fun onShareClicked(uploadEvent: UploadEvent) { + uploadsViewModel.handle(RoomUploadsAction.Share(uploadEvent)) + } + + override fun invalidate() = withState(uploadsViewModel) { 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 new file mode 100644 index 0000000000..60f966e7d2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/files/UploadsFileController.kt @@ -0,0 +1,91 @@ +/* + * 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.epoxy.VisibilityState +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.loadingItem +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 stringProvider: StringProvider, + private val dateFormatter: VectorDateFormatter +) : TypedEpoxyController() { + + interface Listener { + fun loadMore() + fun onOpenClicked(uploadEvent: UploadEvent) + fun onDownloadClicked(uploadEvent: UploadEvent) + fun onShareClicked(uploadEvent: UploadEvent) + } + + var listener: Listener? = null + + private var idx = 0 + + init { + setData(null) + } + + override fun buildModels(data: RoomUploadsViewState?) { + data ?: return + + 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() + } + } + } + } + } + + private fun buildFileItems(fileEvents: List) { + fileEvents.forEach { uploadEvent -> + uploadsFileItem { + id(uploadEvent.eventId) + title(uploadEvent.contentWithAttachmentContent.body) + subtitle(stringProvider.getString(R.string.uploads_files_subtitle, + uploadEvent.senderInfo.disambiguatedDisplayName, + dateFormatter.formatRelativeDateTime(uploadEvent.root.originServerTs))) + listener(object : UploadsFileItem.Listener { + override fun onItemClicked() { + listener?.onOpenClicked(uploadEvent) + } + + override fun onDownloadClicked() { + listener?.onDownloadClicked(uploadEvent) + } + + override fun onShareClicked() { + listener?.onShareClicked(uploadEvent) + } + }) + } + } + } +} 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/media/Config.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/Config.kt new file mode 100644 index 0000000000..50d4feff55 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/Config.kt @@ -0,0 +1,20 @@ +/* + * 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 + +// 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..a4e6c61238 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt @@ -0,0 +1,121 @@ +/* + * 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.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_state_view_recycler.* +import javax.inject.Inject + +class RoomUploadsMediaFragment @Inject constructor( + private val controller: UploadsMediaController, + private val dimensionConverter: DimensionConverter +) : VectorBaseFragment(), + UploadsMediaController.Listener, + StateView.EventCallback { + + private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class) + + override fun getLayoutResId() = R.layout.fragment_generic_state_view_recycler + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + genericStateViewListStateView.contentView = genericStateViewListRecycler + genericStateViewListStateView.eventCallback = this + + genericStateViewListRecycler.trackItemsVisibilityChange() + genericStateViewListRecycler.layoutManager = GridLayoutManager(context, getNumberOfColumns()) + genericStateViewListRecycler.adapter = controller.adapter + genericStateViewListRecycler.setHasFixedSize(true) + + controller.listener = this + } + + private fun getNumberOfColumns(): Int { + val displayMetrics = DisplayMetrics() + requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) + return dimensionConverter.pxToDp(displayMetrics.widthPixels) / IMAGE_SIZE_DP + } + + override fun onDestroyView() { + super.onDestroyView() + genericStateViewListRecycler.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 loadMore() { + uploadsViewModel.handle(RoomUploadsAction.LoadMore) + } + + override fun onRetryClicked() { + uploadsViewModel.handle(RoomUploadsAction.Retry) + } + + override fun invalidate() = withState(uploadsViewModel) { 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/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..cd3e401dc5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/uploads/media/UploadsMediaController.kt @@ -0,0 +1,148 @@ +/* + * 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.epoxy.VisibilityState +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.core.epoxy.squareLoadingItem +import im.vector.riotx.core.error.ErrorFormatter +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 +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 stringProvider: StringProvider, + dimensionConverter: DimensionConverter +) : TypedEpoxyController() { + + interface Listener { + fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) + fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) + fun loadMore() + } + + var listener: Listener? = null + + private var idx = 0 + + private val itemSize = dimensionConverter.dpToPx(IMAGE_SIZE_DP) + + init { + setData(null) + } + + override fun buildModels(data: RoomUploadsViewState?) { + data ?: return + + 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() + } + } + } + } + } + + private fun buildMediaItems(mediaEvents: List) { + mediaEvents.forEach { uploadEvent -> + when (uploadEvent.contentWithAttachmentContent.msgType) { + MessageType.MSGTYPE_IMAGE -> { + val data = uploadEvent.toImageContentRendererData() ?: return@forEach + uploadsImageItem { + id(uploadEvent.eventId) + imageContentRenderer(imageContentRenderer) + data(data) + listener(object : UploadsImageItem.Listener { + override fun onItemClicked(view: View, data: ImageContentRenderer.Data) { + listener?.onOpenImageClicked(view, data) + } + }) + } + } + MessageType.MSGTYPE_VIDEO -> { + val data = uploadEvent.toVideoContentRendererData() ?: return@forEach + uploadsVideoItem { + id(uploadEvent.eventId) + imageContentRenderer(imageContentRenderer) + data(data) + listener(object : UploadsVideoItem.Listener { + override fun onItemClicked(view: View, data: VideoContentRenderer.Data) { + listener?.onOpenVideoClicked(view, data) + } + }) + } + } + } + } + } + + private fun UploadEvent.toImageContentRendererData(): ImageContentRenderer.Data? { + val messageContent = (contentWithAttachmentContent as? MessageImageContent) ?: 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 UploadEvent.toVideoContentRendererData(): VideoContentRenderer.Data? { + val messageContent = (contentWithAttachmentContent as? MessageVideoContent) ?: 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/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/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/fragment_room_uploads.xml b/vector/src/main/res/layout/fragment_room_uploads.xml new file mode 100644 index 0000000000..5e289d4724 --- /dev/null +++ b/vector/src/main/res/layout/fragment_room_uploads.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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..464816d74a --- /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..97d7529c13 --- /dev/null +++ b/vector/src/main/res/layout/item_uploads_video.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file 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 0a9faa8bce..36532f25e3 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1788,6 +1788,13 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming "Sticker" Couldn\'t handle share data + MEDIA + There are no media in this room + FILES + + %1$s at %2$s + There are no files in this room + "It's spam" "It's inappropriate" "Custom report…"