mirror of
https://github.com/element-hq/element-android
synced 2024-12-17 23:02:48 +03:00
Merge pull request #1387 from vector-im/feature/attachments_list
Attachments list
This commit is contained in:
commit
ae318a835e
75 changed files with 2005 additions and 198 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.database.mapper
|
|||
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||
|
||||
import im.vector.matrix.android.api.session.room.sender.SenderInfo
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import javax.inject.Inject
|
||||
|
@ -41,9 +41,12 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
|
|||
annotations = timelineEventEntity.annotations?.asDomain(),
|
||||
localId = timelineEventEntity.localId,
|
||||
displayIndex = timelineEventEntity.displayIndex,
|
||||
senderName = timelineEventEntity.senderName,
|
||||
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
|
||||
senderAvatar = timelineEventEntity.senderAvatar,
|
||||
senderInfo = SenderInfo(
|
||||
userId = timelineEventEntity.root?.sender ?: "",
|
||||
displayName = timelineEventEntity.senderName,
|
||||
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
|
||||
avatarUrl = timelineEventEntity.senderAvatar
|
||||
),
|
||||
readReceipts = readReceipts
|
||||
?.distinctBy {
|
||||
it.user
|
||||
|
|
|
@ -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 = ""
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -23,4 +23,6 @@ internal interface TokenChunkEvent {
|
|||
val end: String?
|
||||
val events: List<Event>
|
||||
val stateEvents: List<Event>
|
||||
|
||||
fun hasMore() = start != end
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -256,7 +256,11 @@ fun shareMedia(context: Context, file: File, mediaMimeType: String?) {
|
|||
sendIntent.type = mediaMimeType
|
||||
sendIntent.putExtra(Intent.EXTRA_STREAM, mediaUri)
|
||||
|
||||
context.startActivity(sendIntent)
|
||||
try {
|
||||
context.startActivity(sendIntent)
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
context.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
navigator.openImageViewer(requireActivity(), mediaData, view) { pairs ->
|
||||
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
|
||||
pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
|
||||
}
|
||||
pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: ""))
|
||||
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
|
||||
pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
|
||||
|
||||
val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
requireActivity(), *pairs.toTypedArray()).toBundle()
|
||||
startActivity(intent, bundle)
|
||||
}
|
||||
|
||||
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {
|
||||
// TODO Use navigator
|
||||
val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
|
||||
startActivity(intent)
|
||||
navigator.openVideoViewer(requireActivity(), mediaData)
|
||||
}
|
||||
|
||||
override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) {
|
||||
|
|
|
@ -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 ?: ""
|
||||
)
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
29
vector/src/main/res/drawable/ic_image.xml
Normal file
29
vector/src/main/res/drawable/ic_image.xml
Normal 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>
|
|
@ -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>
|
86
vector/src/main/res/layout/fragment_room_uploads.xml
Normal file
86
vector/src/main/res/layout/fragment_room_uploads.xml
Normal 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>
|
23
vector/src/main/res/layout/item_loading_square.xml
Normal file
23
vector/src/main/res/layout/item_loading_square.xml
Normal 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>
|
75
vector/src/main/res/layout/item_uploads_file.xml
Normal file
75
vector/src/main/res/layout/item_uploads_file.xml
Normal 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>
|
22
vector/src/main/res/layout/item_uploads_image.xml
Normal file
22
vector/src/main/res/layout/item_uploads_image.xml
Normal 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>
|
31
vector/src/main/res/layout/item_uploads_video.xml
Normal file
31
vector/src/main/res/layout/item_uploads_video.xml
Normal 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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue