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 ✨:
- Identity server support (#607)
- Switch language support (#41)
- Display list of attachments of a room (#860)
Improvements 🙌:
- Better connectivity lost indicator when airplane mode is on

View file

@ -220,3 +220,11 @@ fun Event.isImageMessage(): Boolean {
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.timeline.TimelineService
import im.vector.matrix.android.api.session.room.typing.TypingService
import im.vector.matrix.android.api.session.room.uploads.UploadsService
import im.vector.matrix.android.api.util.Optional
/**
@ -42,6 +43,7 @@ interface Room :
TypingService,
MembershipService,
StateService,
UploadsService,
ReportingService,
RelationService,
RoomCryptoService,

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.MessageStickerContent
import im.vector.matrix.android.api.session.room.model.message.isReply
import im.vector.matrix.android.api.session.room.sender.SenderInfo
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
@ -39,9 +40,7 @@ data class TimelineEvent(
val localId: Long,
val eventId: String,
val displayIndex: Int,
val senderName: String?,
val isUniqueDisplayName: Boolean,
val senderAvatar: String?,
val senderInfo: SenderInfo,
val annotations: EventAnnotationsSummary? = null,
val readReceipts: List<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.
* @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.RoomSummary
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.room.sender.SenderInfo
import im.vector.matrix.android.api.session.user.model.User
import java.util.Locale
@ -154,3 +155,5 @@ fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlia
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl)
fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl)

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

View file

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

View file

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

View file

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

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.JsonClass
import im.vector.matrix.android.internal.di.MoshiProvider
/**
* Represents "Filter" as mentioned in the SPEC
* Class which can be parsed to a filter json string. Used for POST and GET
* Have a look here for further information:
* https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter
*/
@JsonClass(generateAdapter = true)
data class Filter(
@Json(name = "limit") val limit: Int? = null,
@Json(name = "senders") val senders: List<String>? = null,
@Json(name = "not_senders") val notSenders: List<String>? = null,
@Json(name = "types") val types: List<String>? = null,
@Json(name = "not_types") val notTypes: List<String>? = null,
@Json(name = "rooms") val rooms: List<String>? = null,
@Json(name = "not_rooms") val notRooms: List<String>? = null
internal data class Filter(
/**
* List of event fields to include. If this list is absent then all fields are included. The entries may
* include '.' characters to indicate sub-fields. So ['content.body'] will include the 'body' field of the
* 'content' object. A literal '.' character in a field name may be escaped using a '\'. A server may
* include more fields than were requested.
*/
@Json(name = "event_fields") val eventFields: List<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
|| senders != null
|| notSenders != null
|| types != null
|| notTypes != null
|| rooms != null
|| notRooms != null)
fun toJSONString(): String {
return MoshiProvider.providesMoshi().adapter(Filter::class.java).toJson(this)
}
}

View file

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

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 {
fun createDefaultFilterBody(): FilterBody {
return FilterUtil.enableLazyLoading(FilterBody(), true)
fun createUploadsFilter(numberOfEvents: Int): RoomEventFilter {
return RoomEventFilter(
limit = numberOfEvents,
containsUrl = true,
types = listOf(EventType.MESSAGE),
lazyLoadMembers = true
)
}
fun createRiotFilterBody(): FilterBody {
return FilterBody(
fun createDefaultFilter(): Filter {
return FilterUtil.enableLazyLoading(Filter(), true)
}
fun createRiotFilter(): Filter {
return Filter(
room = RoomFilter(
timeline = createRiotTimelineFilter(),
state = createRiotStateFilter()

View file

@ -21,12 +21,12 @@ internal interface FilterRepository {
/**
* Return true if the filterBody has changed, or need to be sent to the server
*/
suspend fun storeFilter(filterBody: FilterBody, roomEventFilter: RoomEventFilter): Boolean
suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean
/**
* Set the filterId of this filter
*/
suspend fun storeFilterId(filterBody: FilterBody, filterId: String)
suspend fun storeFilterId(filter: Filter, filterId: String)
/**
* Return filter json or filter id

View file

@ -24,5 +24,10 @@ import com.squareup.moshi.JsonClass
*/
@JsonClass(generateAdapter = true)
data class FilterResponse(
/**
* Required. The ID of the filter that was created. Cannot start with a { as this character
* is used to determine if the filter provided is inline JSON or a previously declared
* filter by homeservers on some APIs.
*/
@Json(name = "filter_id") val filterId: String
)

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

View file

@ -25,14 +25,46 @@ import im.vector.matrix.android.internal.di.MoshiProvider
*/
@JsonClass(generateAdapter = true)
data class RoomEventFilter(
@Json(name = "limit") var limit: Int? = null,
/**
* The maximum number of events to return.
*/
@Json(name = "limit") val limit: Int? = null,
/**
* A list of sender IDs to exclude. If this list is absent then no senders are excluded. A matching sender will
* be excluded even if it is listed in the 'senders' filter.
*/
@Json(name = "not_senders") val notSenders: List<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,
/**
* 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 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 room IDs to include. If this list is absent then all rooms are included.
*/
@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,
/**
* If true, includes only events with a url key in their content. If false, excludes those events. If omitted, url
* key is not considered for filtering.
*/
@Json(name = "contains_url") val containsUrl: Boolean? = null,
/**
* If true, enables lazy-loading of membership events. See Lazy-loading room members for more information. Defaults to false.
*/
@Json(name = "lazy_load_members") val lazyLoadMembers: Boolean? = null
) {

View file

@ -24,12 +24,37 @@ import com.squareup.moshi.JsonClass
*/
@JsonClass(generateAdapter = true)
data class RoomFilter(
/**
* A list of room IDs to exclude. If this list is absent then no rooms are excluded.
* A matching room will be excluded even if it is listed in the 'rooms' filter.
* This filter is applied before the filters in ephemeral, state, timeline or account_data
*/
@Json(name = "not_rooms") val notRooms: List<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,
/**
* The events that aren't recorded in the room history, e.g. typing and receipts, to include for rooms.
*/
@Json(name = "ephemeral") val ephemeral: RoomEventFilter? = null,
/**
* Include rooms that the user has left in the sync, default false
*/
@Json(name = "include_leave") val includeLeave: Boolean? = null,
/**
* The state events to include for rooms.
* Developer remark: StateFilter is exactly the same than RoomEventFilter
*/
@Json(name = "state") val state: RoomEventFilter? = null,
/**
* The message and state update events to include for rooms.
*/
@Json(name = "timeline") val timeline: RoomEventFilter? = null,
/**
* The per user account data to include for rooms.
*/
@Json(name = "account_data") val accountData: RoomEventFilter? = null
) {

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

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

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

View file

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

View file

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

View file

@ -23,4 +23,6 @@ internal interface TokenChunkEvent {
val end: String?
val events: 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)
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) {
handleReachEnd(realm, roomId, direction, currentChunk)
} else {
handlePagination(realm, roomId, direction, receivedChunk, currentChunk)

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

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.members.RoomMemberListFragment
import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment
import im.vector.riotx.features.roomprofile.uploads.files.RoomUploadsFilesFragment
import im.vector.riotx.features.roomprofile.uploads.media.RoomUploadsMediaFragment
import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment
import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment
import im.vector.riotx.features.settings.VectorSettingsLabsFragment
@ -96,9 +99,9 @@ import im.vector.riotx.features.settings.locale.LocalePickerFragment
import im.vector.riotx.features.settings.push.PushGatewaysFragment
import im.vector.riotx.features.share.IncomingShareFragment
import im.vector.riotx.features.signout.soft.SoftLogoutFragment
import im.vector.riotx.features.terms.ReviewTermsFragment
import im.vector.riotx.features.userdirectory.KnownUsersFragment
import im.vector.riotx.features.userdirectory.UserDirectoryFragment
import im.vector.riotx.features.terms.ReviewTermsFragment
@Module
interface FragmentModule {
@ -308,6 +311,21 @@ interface FragmentModule {
@FragmentKey(RoomMemberListFragment::class)
fun bindRoomMemberListFragment(fragment: RoomMemberListFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomUploadsFragment::class)
fun bindRoomUploadsFragment(fragment: RoomUploadsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomUploadsMediaFragment::class)
fun bindRoomUploadsMediaFragment(fragment: RoomUploadsMediaFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomUploadsFilesFragment::class)
fun bindRoomUploadsFilesFragment(fragment: RoomUploadsFilesFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomSettingsFragment::class)

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.RecyclerView
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyVisibilityTracker
import im.vector.riotx.R
import im.vector.riotx.features.themes.ThemeUtils
@ -61,3 +62,5 @@ fun RecyclerView.configureWith(epoxyController: EpoxyController,
fun RecyclerView.cleanup() {
adapter = null
}
fun RecyclerView.trackItemsVisibilityChange() = EpoxyVisibilityTracker().attach(this)

View file

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

View file

@ -16,5 +16,7 @@
package im.vector.riotx.core.ui.model
import androidx.annotation.Px
// android.util.Size in API 21+
data class Size(val width: Int, val height: Int)
data class Size(@Px val width: Int, @Px val height: Int)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,6 +29,7 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.ORIENTATION
import com.github.piasy.biv.view.BigImageView
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.glide.GlideRequest
@ -65,6 +66,18 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
STICKER
}
/**
* For gallery
*/
fun render(data: Data, imageView: ImageView, size: Int) {
// a11y
imageView.contentDescription = data.filename
createGlideRequest(data, Mode.THUMBNAIL, imageView, Size(size, size))
.placeholder(R.drawable.ic_image)
.into(imageView)
}
fun render(data: Data, mode: Mode, imageView: ImageView) {
val size = processSize(data, mode)
imageView.layoutParams.width = size.width

View file

@ -19,9 +19,12 @@ package im.vector.riotx.features.navigation
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Build
import android.view.View
import android.view.Window
import androidx.core.app.ActivityOptionsCompat
import androidx.core.app.TaskStackBuilder
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
@ -45,6 +48,10 @@ import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.invite.InviteUsersToRoomActivity
import im.vector.riotx.features.media.BigImageViewerActivity
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.ImageMediaViewerActivity
import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.media.VideoMediaViewerActivity
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
@ -215,6 +222,29 @@ class DefaultNavigator @Inject constructor(
fragment.startActivityForResult(intent, requestCode)
}
override fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList<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) {
if (buildTask) {
val stackBuilder = TaskStackBuilder.create(context)

View file

@ -19,10 +19,13 @@ package im.vector.riotx.features.navigation
import android.app.Activity
import android.content.Context
import android.view.View
import androidx.core.util.Pair
import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.terms.ReviewTermsActivity
@ -76,4 +79,8 @@ interface Navigator {
baseUrl: String,
token: String?,
requestCode: Int = ReviewTermsActivity.TERMS_REQUEST_CODE)
fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList<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
val body = displayableEventFormatter.format(event, false)
val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
val senderDisplayName = event.getDisambiguatedDisplayName()
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
val notifiableEvent = NotifiableMessageEvent(
eventId = event.root.eventId!!,
@ -126,7 +126,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
val body = displayableEventFormatter.format(event, false).toString()
val roomName = room.roomSummary()?.displayName ?: ""
val senderDisplayName = event.getDisambiguatedDisplayName()
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
val notifiableEvent = NotifiableMessageEvent(
eventId = event.root.eventId!!,
@ -151,7 +151,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
ContentUrlResolver.ThumbnailMethod.SCALE)
notifiableEvent.senderAvatarPath = session.contentUrlResolver()
.resolveThumbnail(event.senderAvatar,
.resolveThumbnail(event.senderInfo.avatarUrl,
250,
250,
ContentUrlResolver.ThumbnailMethod.SCALE)

View file

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

View file

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

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_height="64dp"
android:layout_gravity="center_horizontal"
android:tint="?riotx_text_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

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="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_inappropriate">"It's inappropriate"</string>
<string name="report_content_custom">"Custom report…"</string>