Merge pull request #1387 from vector-im/feature/attachments_list

Attachments list
This commit is contained in:
Benoit Marty 2020-05-25 17:29:54 +02:00 committed by GitHub
commit ae318a835e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 2005 additions and 198 deletions

View file

@ -4,6 +4,7 @@ Changes in RiotX 0.21.0 (2020-XX-XX)
Features ✨: Features ✨:
- Identity server support (#607) - Identity server support (#607)
- Switch language support (#41) - Switch language support (#41)
- Display list of attachments of a room (#860)
Improvements 🙌: Improvements 🙌:
- Better connectivity lost indicator when airplane mode is on - Better connectivity lost indicator when airplane mode is on

View file

@ -220,3 +220,11 @@ fun Event.isImageMessage(): Boolean {
else -> false else -> false
} }
} }
fun Event.isVideoMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
MessageType.MSGTYPE_VIDEO -> true
else -> false
}
}

View file

@ -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.state.StateService
import im.vector.matrix.android.api.session.room.timeline.TimelineService 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.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.Optional
/** /**
@ -42,6 +43,7 @@ interface Room :
TypingService, TypingService,
MembershipService, MembershipService,
StateService, StateService,
UploadsService,
ReportingService, ReportingService,
RelationService, RelationService,
RoomCryptoService, RoomCryptoService,

View file

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

View file

@ -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.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageStickerContent 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.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.api.util.ContentUtils.extractUsefulTextFromReply
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
@ -39,9 +40,7 @@ data class TimelineEvent(
val localId: Long, val localId: Long,
val eventId: String, val eventId: String,
val displayIndex: Int, val displayIndex: Int,
val senderName: String?, val senderInfo: SenderInfo,
val isUniqueDisplayName: Boolean,
val senderAvatar: String?,
val annotations: EventAnnotationsSummary? = null, val annotations: EventAnnotationsSummary? = null,
val readReceipts: List<ReadReceipt> = emptyList() val readReceipts: List<ReadReceipt> = 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. * Get the metadata associated with a key.
* @param key the key to get the metadata * @param key the key to get the metadata

View file

@ -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<UploadEvent>,
// token to get more events
val nextToken: String,
// True if there are more event to load
val hasMore: Boolean
)

View file

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

View file

@ -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<GetUploadsResult>): Cancelable
}

View file

@ -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.RoomMemberSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary 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.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.room.sender.SenderInfo
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import java.util.Locale import java.util.Locale
@ -154,3 +155,5 @@ fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlia
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl) fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl)
fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl)

View file

@ -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.events.model.Event
import im.vector.matrix.android.api.session.room.model.ReadReceipt 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.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import javax.inject.Inject import javax.inject.Inject
@ -41,9 +41,12 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
annotations = timelineEventEntity.annotations?.asDomain(), annotations = timelineEventEntity.annotations?.asDomain(),
localId = timelineEventEntity.localId, localId = timelineEventEntity.localId,
displayIndex = timelineEventEntity.displayIndex, displayIndex = timelineEventEntity.displayIndex,
senderName = timelineEventEntity.senderName, senderInfo = SenderInfo(
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, userId = timelineEventEntity.root?.sender ?: "",
senderAvatar = timelineEventEntity.senderAvatar, displayName = timelineEventEntity.senderName,
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
avatarUrl = timelineEventEntity.senderAvatar
),
readReceipts = readReceipts readReceipts = readReceipts
?.distinctBy { ?.distinctBy {
it.user it.user

View file

@ -35,7 +35,7 @@ internal fun FilterEntity.Companion.get(realm: Realm): FilterEntity? {
internal fun FilterEntity.Companion.getOrCreate(realm: Realm): FilterEntity { internal fun FilterEntity.Companion.getOrCreate(realm: Realm): FilterEntity {
return get(realm) ?: realm.createObject<FilterEntity>() return get(realm) ?: realm.createObject<FilterEntity>()
.apply { .apply {
filterBodyJson = FilterFactory.createDefaultFilterBody().toJSONString() filterBodyJson = FilterFactory.createDefaultFilter().toJSONString()
roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString() roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString()
filterId = "" filterId = ""
} }

View file

@ -28,25 +28,25 @@ import javax.inject.Inject
internal class DefaultFilterRepository @Inject constructor(private val monarchy: Monarchy) : FilterRepository { 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 -> 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 has changed, or no filter Id yet
filter == null filterEntity == null
|| filter.filterBodyJson != filterBody.toJSONString() || filterEntity.filterBodyJson != filter.toJSONString()
|| filter.filterId.isBlank() || filterEntity.filterId.isBlank()
}.also { hasChanged -> }.also { hasChanged ->
if (hasChanged) { if (hasChanged) {
// Filter is new or has changed, store it and reset the filter Id. // 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 // This has to be done outside of the Realm.use(), because awaitTransaction change the current thread
monarchy.awaitTransaction { realm -> monarchy.awaitTransaction { realm ->
// We manage only one filter for now // We manage only one filter for now
val filterBodyJson = filterBody.toJSONString() val filterJson = filter.toJSONString()
val roomEventFilterJson = roomEventFilter.toJSONString() val roomEventFilterJson = roomEventFilter.toJSONString()
val filterEntity = FilterEntity.getOrCreate(realm) val filterEntity = FilterEntity.getOrCreate(realm)
filterEntity.filterBodyJson = filterBodyJson filterEntity.filterBodyJson = filterJson
filterEntity.roomEventFilterJson = roomEventFilterJson filterEntity.roomEventFilterJson = roomEventFilterJson
// Reset filterId // Reset filterId
filterEntity.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 { monarchy.awaitTransaction {
// We manage only one filter for now // 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 // Update the filter id, only if the filter body matches
it.where<FilterEntity>() it.where<FilterEntity>()
.equalTo(FilterEntityFields.FILTER_BODY_JSON, filterBodyJson) .equalTo(FilterEntityFields.FILTER_BODY_JSON, filterJson)
?.findFirst() ?.findFirst()
?.filterId = filterId ?.filterId = filterId
} }

View file

@ -43,10 +43,10 @@ internal class DefaultSaveFilterTask @Inject constructor(
override suspend fun execute(params: SaveFilterTask.Params) { override suspend fun execute(params: SaveFilterTask.Params) {
val filterBody = when (params.filterPreset) { val filterBody = when (params.filterPreset) {
FilterService.FilterPreset.RiotFilter -> { FilterService.FilterPreset.RiotFilter -> {
FilterFactory.createRiotFilterBody() FilterFactory.createRiotFilter()
} }
FilterService.FilterPreset.NoFilter -> { FilterService.FilterPreset.NoFilter -> {
FilterFactory.createDefaultFilterBody() FilterFactory.createDefaultFilter()
} }
} }
val roomFilter = when (params.filterPreset) { val roomFilter = when (params.filterPreset) {

View file

@ -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<String>? = 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<String>? = 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<String>? = 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<String>? = null
) {
fun hasData(): Boolean {
return limit != null
|| senders != null
|| notSenders != null
|| types != null
|| notTypes != null
}
}

View file

@ -17,28 +17,42 @@ package im.vector.matrix.android.internal.session.filter
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass 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 * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Filter( internal data class Filter(
@Json(name = "limit") val limit: Int? = null, /**
@Json(name = "senders") val senders: List<String>? = null, * List of event fields to include. If this list is absent then all fields are included. The entries may
@Json(name = "not_senders") val notSenders: List<String>? = null, * include '.' characters to indicate sub-fields. So ['content.body'] will include the 'body' field of the
@Json(name = "types") val types: List<String>? = null, * 'content' object. A literal '.' character in a field name may be escaped using a '\'. A server may
@Json(name = "not_types") val notTypes: List<String>? = null, * include more fields than were requested.
@Json(name = "rooms") val rooms: List<String>? = null, */
@Json(name = "not_rooms") val notRooms: List<String>? = null @Json(name = "event_fields") val eventFields: List<String>? = 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 fun toJSONString(): String {
|| senders != null return MoshiProvider.providesMoshi().adapter(Filter::class.java).toJson(this)
|| notSenders != null
|| types != null
|| notTypes != null
|| rooms != null
|| notRooms != null)
} }
} }

View file

@ -32,7 +32,7 @@ internal interface FilterApi {
* @param body the Json representation of a FilterBody object * @param body the Json representation of a FilterBody object
*/ */
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter")
fun uploadFilter(@Path("userId") userId: String, @Body body: FilterBody): Call<FilterResponse> fun uploadFilter(@Path("userId") userId: String, @Body body: Filter): Call<FilterResponse>
/** /**
* Gets a filter with a given filterId from the homeserver * Gets a filter with a given filterId from the homeserver
@ -42,6 +42,5 @@ internal interface FilterApi {
* @return Filter * @return Filter
*/ */
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter/{filterId}") @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter/{filterId}")
fun getFilterById(@Path("userId") userId: String, @Path("filterId") fun getFilterById(@Path("userId") userId: String, @Path("filterId") filterId: String): Call<Filter>
filterId: String): Call<FilterBody>
} }

View file

@ -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<String>? = 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)
}
}

View file

@ -20,12 +20,21 @@ import im.vector.matrix.android.api.session.events.model.EventType
internal object FilterFactory { internal object FilterFactory {
fun createDefaultFilterBody(): FilterBody { fun createUploadsFilter(numberOfEvents: Int): RoomEventFilter {
return FilterUtil.enableLazyLoading(FilterBody(), true) return RoomEventFilter(
limit = numberOfEvents,
containsUrl = true,
types = listOf(EventType.MESSAGE),
lazyLoadMembers = true
)
} }
fun createRiotFilterBody(): FilterBody { fun createDefaultFilter(): Filter {
return FilterBody( return FilterUtil.enableLazyLoading(Filter(), true)
}
fun createRiotFilter(): Filter {
return Filter(
room = RoomFilter( room = RoomFilter(
timeline = createRiotTimelineFilter(), timeline = createRiotTimelineFilter(),
state = createRiotStateFilter() state = createRiotStateFilter()

View file

@ -21,12 +21,12 @@ internal interface FilterRepository {
/** /**
* Return true if the filterBody has changed, or need to be sent to the server * 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 * 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 * Return filter json or filter id

View file

@ -24,5 +24,10 @@ import com.squareup.moshi.JsonClass
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class FilterResponse( 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 @Json(name = "filter_id") val filterId: String
) )

View file

@ -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})} * {"room":{"state":{"lazy_load_members":true})}
* *
* @param filterBody filterBody to patch * @param filter filter to patch
* @param useLazyLoading true to enable lazy loading * @param useLazyLoading true to enable lazy loading
*/ */
fun enableLazyLoading(filterBody: FilterBody, useLazyLoading: Boolean): FilterBody { fun enableLazyLoading(filter: Filter, useLazyLoading: Boolean): Filter {
if (useLazyLoading) { if (useLazyLoading) {
// Enable lazy loading // Enable lazy loading
return filterBody.copy( return filter.copy(
room = filterBody.room?.copy( room = filter.room?.copy(
state = filterBody.room.state?.copy(lazyLoadMembers = true) state = filter.room.state?.copy(lazyLoadMembers = true)
?: RoomEventFilter(lazyLoadMembers = true) ?: RoomEventFilter(lazyLoadMembers = true)
) )
?: RoomFilter(state = RoomEventFilter(lazyLoadMembers = true)) ?: RoomFilter(state = RoomEventFilter(lazyLoadMembers = true))
) )
} else { } else {
val newRoomEventFilter = filterBody.room?.state?.copy(lazyLoadMembers = null)?.takeIf { it.hasData() } val newRoomEventFilter = filter.room?.state?.copy(lazyLoadMembers = null)?.takeIf { it.hasData() }
val newRoomFilter = filterBody.room?.copy(state = newRoomEventFilter)?.takeIf { it.hasData() } val newRoomFilter = filter.room?.copy(state = newRoomEventFilter)?.takeIf { it.hasData() }
return filterBody.copy( return filter.copy(
room = newRoomFilter room = newRoomFilter
) )
} }

View file

@ -25,14 +25,46 @@ import im.vector.matrix.android.internal.di.MoshiProvider
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class RoomEventFilter( 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<String>? = null, @Json(name = "not_senders") val notSenders: List<String>? = 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<String>? = null, @Json(name = "not_types") val notTypes: List<String>? = null,
/**
* A list of senders IDs to include. If this list is absent then all senders are included.
*/
@Json(name = "senders") val senders: List<String>? = null, @Json(name = "senders") val senders: List<String>? = 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<String>? = null, @Json(name = "types") val types: List<String>? = null,
/**
* A list of room IDs to include. If this list is absent then all rooms are included.
*/
@Json(name = "rooms") val rooms: List<String>? = null, @Json(name = "rooms") val rooms: List<String>? = 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<String>? = null, @Json(name = "not_rooms") val notRooms: List<String>? = 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, @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 @Json(name = "lazy_load_members") val lazyLoadMembers: Boolean? = null
) { ) {

View file

@ -24,12 +24,37 @@ import com.squareup.moshi.JsonClass
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class RoomFilter( 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<String>? = null, @Json(name = "not_rooms") val notRooms: List<String>? = 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<String>? = null, @Json(name = "rooms") val rooms: List<String>? = 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, @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, @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, @Json(name = "state") val state: RoomEventFilter? = null,
/**
* The message and state update events to include for rooms.
*/
@Json(name = "timeline") val timeline: RoomEventFilter? = null, @Json(name = "timeline") val timeline: RoomEventFilter? = null,
/**
* The per user account data to include for rooms.
*/
@Json(name = "account_data") val accountData: RoomEventFilter? = null @Json(name = "account_data") val accountData: RoomEventFilter? = null
) { ) {

View file

@ -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.state.StateService
import im.vector.matrix.android.api.session.room.timeline.TimelineService 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.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.Optional
import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM 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 sendService: SendService,
private val draftService: DraftService, private val draftService: DraftService,
private val stateService: StateService, private val stateService: StateService,
private val uploadsService: UploadsService,
private val reportingService: ReportingService, private val reportingService: ReportingService,
private val readService: ReadService, private val readService: ReadService,
private val typingService: TypingService, private val typingService: TypingService,
@ -68,6 +70,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
SendService by sendService, SendService by sendService,
DraftService by draftService, DraftService by draftService,
StateService by stateService, StateService by stateService,
UploadsService by uploadsService,
ReportingService by reportingService, ReportingService by reportingService,
ReadService by readService, ReadService by readService,
TypingService by typingService, TypingService by typingService,

View file

@ -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.state.SendStateTask
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService 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.typing.DefaultTypingService
import im.vector.matrix.android.internal.session.room.uploads.DefaultUploadsService
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import javax.inject.Inject import javax.inject.Inject
@ -47,6 +48,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
private val sendServiceFactory: DefaultSendService.Factory, private val sendServiceFactory: DefaultSendService.Factory,
private val draftServiceFactory: DefaultDraftService.Factory, private val draftServiceFactory: DefaultDraftService.Factory,
private val stateServiceFactory: DefaultStateService.Factory, private val stateServiceFactory: DefaultStateService.Factory,
private val uploadsServiceFactory: DefaultUploadsService.Factory,
private val reportingServiceFactory: DefaultReportingService.Factory, private val reportingServiceFactory: DefaultReportingService.Factory,
private val readServiceFactory: DefaultReadService.Factory, private val readServiceFactory: DefaultReadService.Factory,
private val typingServiceFactory: DefaultTypingService.Factory, private val typingServiceFactory: DefaultTypingService.Factory,
@ -66,6 +68,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
sendService = sendServiceFactory.create(roomId), sendService = sendServiceFactory.create(roomId),
draftService = draftServiceFactory.create(roomId), draftService = draftServiceFactory.create(roomId),
stateService = stateServiceFactory.create(roomId), stateService = stateServiceFactory.create(roomId),
uploadsService = uploadsServiceFactory.create(roomId),
reportingService = reportingServiceFactory.create(roomId), reportingService = reportingServiceFactory.create(roomId),
readService = readServiceFactory.create(roomId), readService = readServiceFactory.create(roomId),
typingService = typingServiceFactory.create(roomId), typingService = typingServiceFactory.create(roomId),

View file

@ -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.timeline.PaginationTask
import im.vector.matrix.android.internal.session.room.typing.DefaultSendTypingTask 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.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 import retrofit2.Retrofit
@Module @Module
@ -156,4 +158,7 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindSendTypingTask(task: DefaultSendTypingTask): SendTypingTask abstract fun bindSendTypingTask(task: DefaultSendTypingTask): SendTypingTask
@Binds
abstract fun bindGetUploadsTask(task: DefaultGetUploadsTask): GetUploadsTask
} }

View file

@ -126,6 +126,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
return name ?: roomId return name ?: roomId
} }
/** See [im.vector.matrix.android.api.session.room.sender.SenderInfo.disambiguatedDisplayName] */
private fun resolveRoomMemberName(roomMemberSummary: RoomMemberSummaryEntity?, private fun resolveRoomMemberName(roomMemberSummary: RoomMemberSummaryEntity?,
roomMemberHelper: RoomMemberHelper): String? { roomMemberHelper: RoomMemberHelper): String? {
if (roomMemberSummary == null) return null if (roomMemberSummary == null) return null

View file

@ -202,7 +202,7 @@ internal class LocalEchoEventFactory @Inject constructor(
permalink, permalink,
stringProvider.getString(R.string.message_reply_to_prefix), stringProvider.getString(R.string.message_reply_to_prefix),
userLink, userLink,
originalEvent.getDisambiguatedDisplayName(), originalEvent.senderInfo.disambiguatedDisplayName,
body.takeFormatted(), body.takeFormatted(),
createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted() createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted()
) )

View file

@ -23,4 +23,6 @@ internal interface TokenChunkEvent {
val end: String? val end: String?
val events: List<Event> val events: List<Event>
val stateEvents: List<Event> val stateEvents: List<Event>
fun hasMore() = start != end
} }

View file

@ -149,7 +149,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
} }
?: ChunkEntity.create(realm, prevToken, nextToken) ?: ChunkEntity.create(realm, prevToken, nextToken)
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) { if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) {
handleReachEnd(realm, roomId, direction, currentChunk) handleReachEnd(realm, roomId, direction, currentChunk)
} else { } else {
handlePagination(realm, roomId, direction, receivedChunk, currentChunk) handlePagination(realm, roomId, direction, receivedChunk, currentChunk)

View file

@ -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<GetUploadsResult>): Cancelable {
return getUploadsTask
.configureWith(GetUploadsTask.Params(roomId, numberOfEvents, since)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
}

View file

@ -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<GetUploadsTask.Params, GetUploadsResult> {
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<PaginationResponse>(eventBus) {
apiCall = roomAPI.getRoomMessagesFrom(params.roomId, since, PaginationDirection.BACKWARDS.value, params.numberOfEvents, filter)
}
var uploadEvents = listOf<UploadEvent>()
val cacheOfSenderInfos = mutableMapOf<String, SenderInfo>()
// 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<MessageContent>() ?: 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()
)
}
}

View file

@ -260,6 +260,7 @@ dependencies {
def autofill_version = "1.0.0" def autofill_version = "1.0.0"
def work_version = '2.3.3' def work_version = '2.3.3'
def arch_version = '2.1.0' def arch_version = '2.1.0'
def lifecycle_version = '2.2.0'
implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android")
implementation project(":matrix-sdk-android-rx") implementation project(":matrix-sdk-android-rx")
@ -282,6 +283,7 @@ dependencies {
implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0" implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0"
implementation "com.squareup.moshi:moshi-adapters:$moshi_version" implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
// Log // Log

View file

@ -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.RoomProfileFragment
import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment
import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment 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.VectorSettingsAdvancedNotificationPreferenceFragment
import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment
import im.vector.riotx.features.settings.VectorSettingsLabsFragment 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.settings.push.PushGatewaysFragment
import im.vector.riotx.features.share.IncomingShareFragment import im.vector.riotx.features.share.IncomingShareFragment
import im.vector.riotx.features.signout.soft.SoftLogoutFragment 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.KnownUsersFragment
import im.vector.riotx.features.userdirectory.UserDirectoryFragment import im.vector.riotx.features.userdirectory.UserDirectoryFragment
import im.vector.riotx.features.terms.ReviewTermsFragment
@Module @Module
interface FragmentModule { interface FragmentModule {
@ -308,6 +311,21 @@ interface FragmentModule {
@FragmentKey(RoomMemberListFragment::class) @FragmentKey(RoomMemberListFragment::class)
fun bindRoomMemberListFragment(fragment: RoomMemberListFragment): Fragment 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 @Binds
@IntoMap @IntoMap
@FragmentKey(RoomSettingsFragment::class) @FragmentKey(RoomSettingsFragment::class)

View file

@ -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<SquareLoadingItem.Holder>() {
class Holder : VectorEpoxyHolder()
}

View file

@ -21,6 +21,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyVisibilityTracker
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.themes.ThemeUtils
@ -61,3 +62,5 @@ fun RecyclerView.configureWith(epoxyController: EpoxyController,
fun RecyclerView.cleanup() { fun RecyclerView.cleanup() {
adapter = null adapter = null
} }
fun RecyclerView.trackItemsVisibilityChange() = EpoxyVisibilityTracker().attach(this)

View file

@ -21,6 +21,7 @@ import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.view.isVisible
import im.vector.riotx.R import im.vector.riotx.R
import kotlinx.android.synthetic.main.view_state.view.* import kotlinx.android.synthetic.main.view_state.view.*
@ -31,6 +32,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
object Content : State() object Content : State()
object Loading : State() object Loading : State()
data class Empty(val title: CharSequence? = null, val image: Drawable? = null, val message: CharSequence? = null) : 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() 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) { 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) { when (newState) {
is State.Content -> { is State.Content -> Unit
progressBar.visibility = View.INVISIBLE is State.Loading -> Unit
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.Empty -> { is State.Empty -> {
progressBar.visibility = View.INVISIBLE
errorView.visibility = View.INVISIBLE
emptyView.visibility = View.VISIBLE
emptyImageView.setImageDrawable(newState.image) emptyImageView.setImageDrawable(newState.image)
emptyMessageView.text = newState.message emptyMessageView.text = newState.message
emptyTitleView.text = newState.title emptyTitleView.text = newState.title
contentView?.visibility = View.INVISIBLE
} }
is State.Error -> { is State.Error -> {
progressBar.visibility = View.INVISIBLE
errorView.visibility = View.VISIBLE
emptyView.visibility = View.INVISIBLE
errorMessageView.text = newState.message errorMessageView.text = newState.message
contentView?.visibility = View.INVISIBLE
} }
} }
} }

View file

@ -16,5 +16,7 @@
package im.vector.riotx.core.ui.model package im.vector.riotx.core.ui.model
import androidx.annotation.Px
// android.util.Size in API 21+ // 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)

View file

@ -17,10 +17,12 @@ package im.vector.riotx.core.utils
import android.content.res.Resources import android.content.res.Resources
import android.util.TypedValue import android.util.TypedValue
import androidx.annotation.Px
import javax.inject.Inject import javax.inject.Inject
class DimensionConverter @Inject constructor(val resources: Resources) { class DimensionConverter @Inject constructor(val resources: Resources) {
@Px
fun dpToPx(dp: Int): Int { fun dpToPx(dp: Int): Int {
return TypedValue.applyDimension( return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, TypedValue.COMPLEX_UNIT_DIP,
@ -29,6 +31,7 @@ class DimensionConverter @Inject constructor(val resources: Resources) {
).toInt() ).toInt()
} }
@Px
fun spToPx(sp: Int): Int { fun spToPx(sp: Int): Int {
return TypedValue.applyDimension( return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, TypedValue.COMPLEX_UNIT_SP,
@ -36,4 +39,8 @@ class DimensionConverter @Inject constructor(val resources: Resources) {
resources.displayMetrics resources.displayMetrics
).toInt() ).toInt()
} }
fun pxToDp(@Px px: Int): Int {
return (px.toFloat() / resources.displayMetrics.density).toInt()
}
} }

View file

@ -256,7 +256,11 @@ fun shareMedia(context: Context, file: File, mediaMimeType: String?) {
sendIntent.type = mediaMimeType sendIntent.type = mediaMimeType
sendIntent.putExtra(Intent.EXTRA_STREAM, mediaUri) 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)
}
} }
} }

View file

@ -22,7 +22,6 @@ import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.Typeface import android.graphics.Typeface
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.text.Spannable import android.text.Spannable
@ -30,12 +29,10 @@ import android.view.HapticFeedbackConstants
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.Window
import android.widget.Toast import android.widget.Toast
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.text.buildSpannedString import androidx.core.text.buildSpannedString
@ -49,7 +46,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail 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.hideKeyboard
import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.extensions.showKeyboard 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.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.intent.getMimeTypeFromUri 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.html.PillImageSpan
import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.invite.VectorInviteView
import im.vector.riotx.features.media.ImageContentRenderer 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.VideoContentRenderer
import im.vector.riotx.features.media.VideoMediaViewerActivity
import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.permalink.NavigationInterceptor import im.vector.riotx.features.permalink.NavigationInterceptor
import im.vector.riotx.features.permalink.PermalinkHandler import im.vector.riotx.features.permalink.PermalinkHandler
@ -463,7 +458,7 @@ class RoomDetailFragment @Inject constructor(
autoCompleter.enterSpecialMode() autoCompleter.enterSpecialMode()
// switch to expanded bar // switch to expanded bar
composerLayout.composerRelatedMessageTitle.apply { composerLayout.composerRelatedMessageTitle.apply {
text = event.getDisambiguatedDisplayName() text = event.senderInfo.disambiguatedDisplayName
setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.senderId))) setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.senderId)))
} }
@ -482,11 +477,7 @@ class RoomDetailFragment @Inject constructor(
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
composerLayout.sendButton.contentDescription = getString(descriptionRes) composerLayout.sendButton.contentDescription = getString(descriptionRes)
avatarRenderer.render( avatarRenderer.render(event.senderInfo.toMatrixItem(), composerLayout.composerRelatedMessageAvatar)
MatrixItem.UserItem(event.root.senderId
?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
composerLayout.composerRelatedMessageAvatar
)
composerLayout.expand { composerLayout.expand {
if (isAdded) { if (isAdded) {
@ -543,8 +534,7 @@ class RoomDetailFragment @Inject constructor(
timelineEventController.callback = this timelineEventController.callback = this
timelineEventController.timeline = roomDetailViewModel.timeline timelineEventController.timeline = roomDetailViewModel.timeline
val epoxyVisibilityTracker = EpoxyVisibilityTracker() recyclerView.trackItemsVisibilityChange()
epoxyVisibilityTracker.attach(recyclerView)
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController) scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController)
@ -998,31 +988,14 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) { override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) {
// TODO Use navigator navigator.openImageViewer(requireActivity(), mediaData, view) { pairs ->
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view)) pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
val pairs = ArrayList<Pair<View, String>>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
requireActivity().window.decorView.findViewById<View>(android.R.id.statusBarBackground)?.let {
pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME))
}
requireActivity().window.decorView.findViewById<View>(android.R.id.navigationBarBackground)?.let {
pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME))
}
} }
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) { override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {
// TODO Use navigator navigator.openVideoViewer(requireActivity(), mediaData)
val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
startActivity(intent)
} }
override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) { override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) {

View file

@ -85,12 +85,10 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) { if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
highlighted = true highlighted = true
} }
val senderAvatar = mergedEvent.senderAvatar
val senderName = mergedEvent.getDisambiguatedDisplayName()
val data = BasedMergedItem.Data( val data = BasedMergedItem.Data(
userId = mergedEvent.root.senderId ?: "", userId = mergedEvent.root.senderId ?: "",
avatarUrl = senderAvatar, avatarUrl = mergedEvent.senderInfo.avatarUrl,
memberName = senderName, memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
localId = mergedEvent.localId, localId = mergedEvent.localId,
eventId = mergedEvent.root.eventId ?: "" eventId = mergedEvent.root.eventId ?: ""
) )
@ -158,12 +156,10 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) { if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
highlighted = true highlighted = true
} }
val senderAvatar = mergedEvent.senderAvatar
val senderName = mergedEvent.getDisambiguatedDisplayName()
val data = BasedMergedItem.Data( val data = BasedMergedItem.Data(
userId = mergedEvent.root.senderId ?: "", userId = mergedEvent.root.senderId ?: "",
avatarUrl = senderAvatar, avatarUrl = mergedEvent.senderInfo.avatarUrl,
memberName = senderName, memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
localId = mergedEvent.localId, localId = mergedEvent.localId,
eventId = mergedEvent.root.eventId ?: "" eventId = mergedEvent.root.eventId ?: ""
) )

View file

@ -45,7 +45,7 @@ class DisplayableEventFormatter @Inject constructor(
return stringProvider.getString(R.string.encrypted_message) return stringProvider.getString(R.string.encrypted_message)
} }
val senderName = timelineEvent.getDisambiguatedDisplayName() val senderName = timelineEvent.senderInfo.disambiguatedDisplayName
when (timelineEvent.root.getClearType()) { when (timelineEvent.root.getClearType()) {
EventType.MESSAGE -> { EventType.MESSAGE -> {

View file

@ -47,20 +47,20 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
fun format(timelineEvent: TimelineEvent): CharSequence? { fun format(timelineEvent: TimelineEvent): CharSequence? {
return when (val type = timelineEvent.root.getClearType()) { 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_CREATE -> formatRoomCreateEvent(timelineEvent.root)
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.getDisambiguatedDisplayName()) EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.MESSAGE, EventType.MESSAGE,
EventType.REACTION, EventType.REACTION,
EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_START,

View file

@ -64,16 +64,14 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
val showInformation = val showInformation =
addDaySeparator addDaySeparator
|| event.senderAvatar != nextEvent?.senderAvatar || event.senderInfo.avatarUrl != nextEvent?.senderInfo?.avatarUrl
|| event.getDisambiguatedDisplayName() != nextEvent?.getDisambiguatedDisplayName() || event.senderInfo.disambiguatedDisplayName != nextEvent?.senderInfo?.disambiguatedDisplayName
|| (nextEvent.root.getClearType() != EventType.MESSAGE && nextEvent.root.getClearType() != EventType.ENCRYPTED) || (nextEvent.root.getClearType() != EventType.MESSAGE && nextEvent.root.getClearType() != EventType.ENCRYPTED)
|| isNextMessageReceivedMoreThanOneHourAgo || isNextMessageReceivedMoreThanOneHourAgo
|| isTileTypeMessage(nextEvent) || isTileTypeMessage(nextEvent)
val time = dateFormatter.formatMessageHour(date) val time = dateFormatter.formatMessageHour(date)
val avatarUrl = event.senderAvatar val formattedMemberName = span(event.senderInfo.disambiguatedDisplayName) {
val memberName = event.getDisambiguatedDisplayName()
val formattedMemberName = span(memberName) {
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId)) textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId))
} }
@ -85,7 +83,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
sendState = event.root.sendState, sendState = event.root.sendState,
time = time, time = time,
ageLocalTS = event.root.ageLocalTs, ageLocalTS = event.root.ageLocalTs,
avatarUrl = avatarUrl, avatarUrl = event.senderInfo.avatarUrl,
memberName = formattedMemberName, memberName = formattedMemberName,
showInformation = showInformation, showInformation = showInformation,
orderedReactionList = event.annotations?.reactionsSummary orderedReactionList = event.annotations?.reactionsSummary

View file

@ -111,7 +111,7 @@ class ViewReactionsViewModel @AssistedInject constructor(@Assisted
event.root.eventId!!, event.root.eventId!!,
summary.key, summary.key,
event.root.senderId ?: "", event.root.senderId ?: "",
event.getDisambiguatedDisplayName(), event.senderInfo.disambiguatedDisplayName,
dateFormatter.formatRelativeDateTime(event.root.originServerTs) dateFormatter.formatRelativeDateTime(event.root.originServerTs)
) )

View file

@ -29,6 +29,7 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.ORIENTATION
import com.github.piasy.biv.view.BigImageView import com.github.piasy.biv.view.BigImageView
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt 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.di.ActiveSessionHolder
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.glide.GlideRequest import im.vector.riotx.core.glide.GlideRequest
@ -65,6 +66,18 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
STICKER 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) { fun render(data: Data, mode: Mode, imageView: ImageView) {
val size = processSize(data, mode) val size = processSize(data, mode)
imageView.layoutParams.width = size.width imageView.layoutParams.width = size.width

View file

@ -19,9 +19,12 @@ package im.vector.riotx.features.navigation
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.view.View import android.view.View
import android.view.Window
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.app.TaskStackBuilder import androidx.core.app.TaskStackBuilder
import androidx.core.util.Pair
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction 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.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.invite.InviteUsersToRoomActivity import im.vector.riotx.features.invite.InviteUsersToRoomActivity
import im.vector.riotx.features.media.BigImageViewerActivity 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.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
@ -215,6 +222,29 @@ class DefaultNavigator @Inject constructor(
fragment.startActivityForResult(intent, requestCode) fragment.startActivityForResult(intent, requestCode)
} }
override fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList<Pair<View, String>>) -> Unit)?) {
val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view))
val pairs = ArrayList<Pair<View, String>>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.decorView.findViewById<View>(android.R.id.statusBarBackground)?.let {
pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME))
}
activity.window.decorView.findViewById<View>(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) { private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {
if (buildTask) { if (buildTask) {
val stackBuilder = TaskStackBuilder.create(context) val stackBuilder = TaskStackBuilder.create(context)

View file

@ -19,10 +19,13 @@ package im.vector.riotx.features.navigation
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import androidx.core.util.Pair
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom 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.session.terms.TermsService
import im.vector.matrix.android.api.util.MatrixItem 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.settings.VectorSettingsActivity
import im.vector.riotx.features.share.SharedData import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.terms.ReviewTermsActivity import im.vector.riotx.features.terms.ReviewTermsActivity
@ -76,4 +79,8 @@ interface Navigator {
baseUrl: String, baseUrl: String,
token: String?, token: String?,
requestCode: Int = ReviewTermsActivity.TERMS_REQUEST_CODE) requestCode: Int = ReviewTermsActivity.TERMS_REQUEST_CODE)
fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList<Pair<View, String>>) -> Unit)?)
fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data)
} }

View file

@ -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 // Ok room is not known in store, but we can still display something
val body = displayableEventFormatter.format(event, false) val body = displayableEventFormatter.format(event, false)
val roomName = stringProvider.getString(R.string.notification_unknown_room_name) val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
val senderDisplayName = event.getDisambiguatedDisplayName() val senderDisplayName = event.senderInfo.disambiguatedDisplayName
val notifiableEvent = NotifiableMessageEvent( val notifiableEvent = NotifiableMessageEvent(
eventId = event.root.eventId!!, eventId = event.root.eventId!!,
@ -126,7 +126,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
val body = displayableEventFormatter.format(event, false).toString() val body = displayableEventFormatter.format(event, false).toString()
val roomName = room.roomSummary()?.displayName ?: "" val roomName = room.roomSummary()?.displayName ?: ""
val senderDisplayName = event.getDisambiguatedDisplayName() val senderDisplayName = event.senderInfo.disambiguatedDisplayName
val notifiableEvent = NotifiableMessageEvent( val notifiableEvent = NotifiableMessageEvent(
eventId = event.root.eventId!!, eventId = event.root.eventId!!,
@ -151,7 +151,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
ContentUrlResolver.ThumbnailMethod.SCALE) ContentUrlResolver.ThumbnailMethod.SCALE)
notifiableEvent.senderAvatarPath = session.contentUrlResolver() notifiableEvent.senderAvatarPath = session.contentUrlResolver()
.resolveThumbnail(event.senderAvatar, .resolveThumbnail(event.senderInfo.avatarUrl,
250, 250,
250, 250,
ContentUrlResolver.ThumbnailMethod.SCALE) ContentUrlResolver.ThumbnailMethod.SCALE)

View file

@ -19,7 +19,6 @@ package im.vector.riotx.features.roomdirectory
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.snackbar.Snackbar 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.cleanup
import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.trackItemsVisibilityChange
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import kotlinx.android.synthetic.main.fragment_public_rooms.* import kotlinx.android.synthetic.main.fragment_public_rooms.*
@ -107,8 +107,7 @@ class PublicRoomsFragment @Inject constructor(
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
val epoxyVisibilityTracker = EpoxyVisibilityTracker() publicRoomsList.trackItemsVisibilityChange()
epoxyVisibilityTracker.attach(publicRoomsList)
publicRoomsList.configureWith(publicRoomsController) publicRoomsList.configureWith(publicRoomsController)
publicRoomsController.callback = this publicRoomsController.callback = this
} }

View file

@ -27,6 +27,7 @@ import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment
import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment
class RoomProfileActivity : VectorBaseActivity(), ToolbarConfigurable { class RoomProfileActivity : VectorBaseActivity(), ToolbarConfigurable {
@ -66,7 +67,7 @@ class RoomProfileActivity : VectorBaseActivity(), ToolbarConfigurable {
} }
private fun openRoomUploads() { private fun openRoomUploads() {
notImplemented("Open room uploads") addFragmentToBackstack(R.id.simpleFragmentContainer, RoomUploadsFragment::class.java, roomProfileArgs)
} }
private fun openRoomSettings() { private fun openRoomSettings() {

View file

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

View file

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

View file

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

View file

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

View file

@ -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<RoomUploadsViewState, RoomUploadsAction, RoomUploadsViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: RoomUploadsViewState): RoomUploadsViewModel
}
companion object : MvRxViewModelFactory<RoomUploadsViewModel, RoomUploadsViewState> {
@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<GetUploadsResult> {
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<File> {
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<File> {
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))
}
}
}
}

View file

@ -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<RoomSummary> = Uninitialized,
// Store cumul of pagination result, grouped by type
val mediaEvents: List<UploadEvent> = emptyList(),
val fileEvents: List<UploadEvent> = emptyList(),
// Current pagination request
val asyncEventsRequest: Async<Unit> = Uninitialized,
// True if more result are available server side
val hasMore: Boolean = true
) : MvRxState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)
}

View file

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

View file

@ -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<RoomUploadsViewState>() {
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<UploadEvent>) {
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)
}
})
}
}
}
}

View file

@ -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<UploadsFileItem.Holder>() {
@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<TextView>(R.id.uploadsFileTitle)
val subtitleView by bind<TextView>(R.id.uploadsFileSubtitle)
val downloadView by bind<View>(R.id.uploadsFileActionDownload)
val shareView by bind<View>(R.id.uploadsFileActionShare)
}
interface Listener {
fun onItemClicked()
fun onDownloadClicked()
fun onShareClicked()
}
}

View file

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

View file

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

View file

@ -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<UploadsImageItem.Holder>() {
@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<ImageView>(R.id.uploadsImagePreview)
}
interface Listener {
fun onItemClicked(view: View, data: ImageContentRenderer.Data)
}
}

View file

@ -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<RoomUploadsViewState>() {
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<UploadEvent>) {
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
)
}
}

View file

@ -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<UploadsVideoItem.Holder>() {
@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<ImageView>(R.id.uploadsVideoPreview)
}
interface Listener {
fun onItemClicked(view: View, data: VideoContentRenderer.Data)
}
}

View file

@ -0,0 +1,29 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M3,5C3,3.8954 3.8954,3 5,3H19C20.1046,3 21,3.8954 21,5V19C21,20.1046 20.1046,21 19,21H5C3.8954,21 3,20.1046 3,19V5Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
<path
android:pathData="M8.5,10C9.3284,10 10,9.3284 10,8.5C10,7.6716 9.3284,7 8.5,7C7.6716,7 7,7.6716 7,8.5C7,9.3284 7.6716,10 8.5,10Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
<path
android:pathData="M21,15L16,10L5,21"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<im.vector.riotx.core.platform.StateView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/genericStateViewListStateView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/genericStateViewListRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="always" />
</im.vector.riotx.core.platform.StateView>

View file

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/roomUploadsCoordinator"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
style="@style/VectorAppBarLayoutStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp">
<androidx.appcompat.widget.Toolbar
android:id="@+id/roomUploadsToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:elevation="4dp"
app:layout_collapseMode="pin">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/roomUploadsToolbarContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/roomUploadsToolbarAvatarImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/roomUploadsDecorationToolbarAvatarImageView"
android:layout_width="24dp"
android:layout_height="24dp"
app:layout_constraintCircle="@+id/roomUploadsToolbarAvatarImageView"
app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="20dp"
tools:ignore="MissingConstraints"
tools:src="@drawable/ic_shield_trusted" />
<im.vector.riotx.core.platform.EllipsizingTextView
android:id="@+id/roomUploadsToolbarTitleView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?vctr_toolbar_primary_text_color"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/roomUploadsToolbarAvatarImageView"
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/matrix.json/data/roomName" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.Toolbar>
<com.google.android.material.tabs.TabLayout
android:id="@+id/roomUploadsTabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/roomUploadsToolbarAvatarImageView"
app:tabGravity="fill"
app:tabMaxWidth="0dp"
app:tabMode="fixed" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/roomUploadsViewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?selectableItemBackground"
android:minHeight="64dp">
<ImageView
android:id="@+id/uploadsFileIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:src="@drawable/ic_file"
android:tint="?riotx_text_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<im.vector.riotx.core.platform.EllipsizingTextView
android:id="@+id/uploadsFileTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:ellipsize="end"
android:maxLines="2"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/uploadsFileSubtitle"
app:layout_constraintEnd_toStartOf="@+id/uploadsFileActionDownload"
app:layout_constraintStart_toEndOf="@+id/uploadsFileIcon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Filename.file" />
<im.vector.riotx.core.platform.EllipsizingTextView
android:id="@+id/uploadsFileSubtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:textColor="?riotx_text_secondary"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/uploadsFileTitle"
app:layout_constraintStart_toStartOf="@+id/uploadsFileTitle"
app:layout_constraintTop_toBottomOf="@+id/uploadsFileTitle"
tools:text="Username at 12:00 on 01/01/01" />
<ImageView
android:id="@+id/uploadsFileActionDownload"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:scaleType="center"
android:src="@drawable/ic_download"
android:tint="?colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/uploadsFileActionShare"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/uploadsFileActionShare"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:scaleType="center"
android:src="@drawable/ic_material_share"
android:tint="?colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?selectableItemBackground">
<ImageView
android:id="@+id/uploadsImagePreview"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="2dp"
android:scaleType="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/backgrounds/scenic" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?selectableItemBackground">
<ImageView
android:id="@+id/uploadsVideoPreview"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="2dp"
android:scaleType="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/backgrounds/scenic" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_material_play_circle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -73,6 +73,7 @@
android:layout_width="64dp" android:layout_width="64dp"
android:layout_height="64dp" android:layout_height="64dp"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:tint="?riotx_text_primary"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"

View file

@ -1788,6 +1788,13 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="attachment_type_sticker">"Sticker"</string> <string name="attachment_type_sticker">"Sticker"</string>
<string name="error_handling_incoming_share">Couldn\'t handle share data</string> <string name="error_handling_incoming_share">Couldn\'t handle share data</string>
<string name="uploads_media_title">MEDIA</string>
<string name="uploads_media_no_result">There are no media in this room</string>
<string name="uploads_files_title">FILES</string>
<!-- First parameter is a username and second is a date Example: "Matthew at 12:00 on 01/01/01" -->
<string name="uploads_files_subtitle">%1$s at %2$s</string>
<string name="uploads_files_no_result">There are no files in this room</string>
<string name="report_content_spam">"It's spam"</string> <string name="report_content_spam">"It's spam"</string>
<string name="report_content_inappropriate">"It's inappropriate"</string> <string name="report_content_inappropriate">"It's inappropriate"</string>
<string name="report_content_custom">"Custom report…"</string> <string name="report_content_custom">"Custom report…"</string>