diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt index a80e92a098..c43ff43819 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt @@ -20,7 +20,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor +import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor @@ -57,7 +57,7 @@ internal class TimelineTest : InstrumentedTest { val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy) val paginationTask = FakePaginationTask(tokenChunkEventPersistor) val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor) - val roomMemberExtractor = RoomMemberExtractor(ROOM_ID) + val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID) val timelineEventFactory = TimelineEventFactory(roomMemberExtractor) return DefaultTimeline(ROOM_ID, initialEventId, monarchy.realmConfiguration, taskExecutor, getContextOfEventTask, timelineEventFactory, paginationTask, null) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt index 82a4a762ec..c8dca8692c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt @@ -27,8 +27,8 @@ data class ContentAttachmentData( val height: Long? = 0, val width: Long? = 0, val name: String? = null, - val path: String? = null, - val mimeType: String? = null, + val path: String, + val mimeType: String, val type: Type ) : Parcelable { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt index 0d88e5faf7..a08060e65f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt @@ -18,9 +18,15 @@ package im.vector.matrix.android.api.session.content interface ContentUploadStateTracker { - fun track(eventId: String, updateListener: UpdateListener) + fun track(key: String, updateListener: UpdateListener) - fun untrack(eventId: String, updateListener: UpdateListener) + fun untrack(key: String, updateListener: UpdateListener) + + fun setFailure(key: String) + + fun setSuccess(key: String) + + fun setProgress(key: String, current: Long, total: Long) interface UpdateListener { fun onUpdate(state: State) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index ca971b653d..58044278e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -29,7 +29,8 @@ data class TimelineEvent( val root: Event, val localId: String, val displayIndex: Int, - val roomMember: RoomMember?, + val senderName: String?, + val senderAvatar: String?, val sendState: SendState ) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index 6df95e107f..284704557d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -56,14 +56,10 @@ internal fun ChunkEntity.merge(roomId: String, if (direction == PaginationDirection.FORWARDS) { this.nextToken = chunkToMerge.nextToken this.isLastForward = chunkToMerge.isLastForward - this.forwardsStateIndex = chunkToMerge.forwardsStateIndex - this.forwardsDisplayIndex = chunkToMerge.forwardsDisplayIndex eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) } else { this.prevToken = chunkToMerge.prevToken this.isLastBackward = chunkToMerge.isLastBackward - this.backwardsStateIndex = chunkToMerge.backwardsStateIndex - this.backwardsDisplayIndex = chunkToMerge.backwardsDisplayIndex eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) } eventsToMerge.forEach { @@ -119,20 +115,20 @@ internal fun ChunkEntity.add(roomId: String, this.displayIndex = currentDisplayIndex this.sendState = SendState.SYNCED } - // We are not using the order of the list, but will be sorting with displayIndex field - events.add(eventEntity) + val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size + events.add(position, eventEntity) } internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> forwardsDisplayIndex - PaginationDirection.BACKWARDS -> backwardsDisplayIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> forwardsDisplayIndex + PaginationDirection.BACKWARDS -> backwardsDisplayIndex + } ?: defaultValue } internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> forwardsStateIndex - PaginationDirection.BACKWARDS -> backwardsStateIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> forwardsStateIndex + PaginationDirection.BACKWARDS -> backwardsStateIndex + } ?: defaultValue } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt index b149cd3867..67ff2d7bc9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt @@ -31,7 +31,7 @@ internal class ContentModule { } scope(DefaultSession.SCOPE) { - ContentUploader(get(), get(), get() as DefaultContentUploadStateTracker) + FileUploader(get(), get()) } scope(DefaultSession.SCOPE) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt index 1e6ca3c1c0..66bd5a8200 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt @@ -23,42 +23,42 @@ import im.vector.matrix.android.api.session.content.ContentUploadStateTracker internal class DefaultContentUploadStateTracker : ContentUploadStateTracker { private val mainHandler = Handler(Looper.getMainLooper()) - private val progressByEvent = mutableMapOf() - private val listenersByEvent = mutableMapOf>() + private val states = mutableMapOf() + private val listeners = mutableMapOf>() - override fun track(eventId: String, updateListener: ContentUploadStateTracker.UpdateListener) { - val listeners = listenersByEvent[eventId] ?: ArrayList() + override fun track(key: String, updateListener: ContentUploadStateTracker.UpdateListener) { + val listeners = listeners[key] ?: ArrayList() listeners.add(updateListener) - listenersByEvent[eventId] = listeners - val currentState = progressByEvent[eventId] ?: ContentUploadStateTracker.State.Idle + this.listeners[key] = listeners + val currentState = states[key] ?: ContentUploadStateTracker.State.Idle mainHandler.post { updateListener.onUpdate(currentState) } } - override fun untrack(eventId: String, updateListener: ContentUploadStateTracker.UpdateListener) { - listenersByEvent[eventId]?.apply { + override fun untrack(key: String, updateListener: ContentUploadStateTracker.UpdateListener) { + listeners[key]?.apply { remove(updateListener) } } - internal fun setFailure(eventId: String) { + override fun setFailure(key: String) { val failure = ContentUploadStateTracker.State.Failure - updateState(eventId, failure) + updateState(key, failure) } - internal fun setSuccess(eventId: String) { + override fun setSuccess(key: String) { val success = ContentUploadStateTracker.State.Success - updateState(eventId, success) + updateState(key, success) } - internal fun setProgress(eventId: String, current: Long, total: Long) { + override fun setProgress(key: String, current: Long, total: Long) { val progressData = ContentUploadStateTracker.State.ProgressData(current, total) - updateState(eventId, progressData) + updateState(key, progressData) } - private fun updateState(eventId: String, state: ContentUploadStateTracker.State) { - progressByEvent[eventId] = state + private fun updateState(key: String, state: ContentUploadStateTracker.State) { + states[key] = state mainHandler.post { - listenersByEvent[eventId]?.also { listeners -> + listeners[key]?.also { listeners -> listeners.forEach { it.onUpdate(state) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt similarity index 51% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentUploader.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt index 42dff4f036..1ec1812709 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt @@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session.content import arrow.core.Try import arrow.core.Try.Companion.raise import im.vector.matrix.android.api.auth.data.SessionParams -import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.network.ProgressRequestBody import okhttp3.HttpUrl @@ -31,44 +30,51 @@ import java.io.File import java.io.IOException -internal class ContentUploader(private val okHttpClient: OkHttpClient, - private val sessionParams: SessionParams, - private val contentUploadProgressTracker: DefaultContentUploadStateTracker) { +internal class FileUploader(private val okHttpClient: OkHttpClient, + private val sessionParams: SessionParams) { + + private val uploadUrl = sessionParams.homeServerConnectionConfig.homeServerUri.toString() + URI_PREFIX_CONTENT_API + "upload" private val moshi = MoshiProvider.providesMoshi() private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java) - fun uploadFile(eventId: String, attachment: ContentAttachmentData): Try { - if (attachment.path == null || attachment.mimeType == null) { - return raise(RuntimeException()) - } - val file = File(attachment.path) - val urlString = sessionParams.homeServerConnectionConfig.homeServerUri.toString() + URI_PREFIX_CONTENT_API + "upload" - val urlBuilder = HttpUrl.parse(urlString)?.newBuilder() - ?: return raise(RuntimeException()) + fun uploadFile(file: File, + filename: String?, + mimeType: String, + progressListener: ProgressRequestBody.Listener? = null): Try { + + val uploadBody = RequestBody.create(MediaType.parse(mimeType), file) + return upload(uploadBody, filename, progressListener) + + } + + fun uploadByteArray(byteArray: ByteArray, + filename: String?, + mimeType: String, + progressListener: ProgressRequestBody.Listener? = null): Try { + + val uploadBody = RequestBody.create(MediaType.parse(mimeType), byteArray) + return upload(uploadBody, filename, progressListener) + + } + + + private fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): Try { + val urlBuilder = HttpUrl.parse(uploadUrl)?.newBuilder() ?: return raise(RuntimeException()) val httpUrl = urlBuilder - .addQueryParameter( - "filename", attachment.name - ).build() + .addQueryParameter("filename", filename) + .build() - val requestBody = RequestBody.create( - MediaType.parse(attachment.mimeType), - file - ) - val progressRequestBody = ProgressRequestBody(requestBody, object : ProgressRequestBody.Listener { - override fun onProgress(current: Long, total: Long) { - contentUploadProgressTracker.setProgress(eventId, current, total) - } - }) + val requestBody = if (progressListener != null) ProgressRequestBody(uploadBody, progressListener) else uploadBody val request = Request.Builder() .url(httpUrl) - .post(progressRequestBody) + .post(requestBody) .build() - val result = Try { + return Try { okHttpClient.newCall(request).execute().use { response -> if (!response.isSuccessful) { throw IOException() @@ -80,11 +86,7 @@ internal class ContentUploader(private val okHttpClient: OkHttpClient, } } } - if (result.isFailure()) { - contentUploadProgressTracker.setFailure(eventId) - } else { - contentUploadProgressTracker.setSuccess(eventId) - } - return result + } + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt new file mode 100644 index 0000000000..8fc6f5f983 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt @@ -0,0 +1,68 @@ +/* + * 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.content + +import android.graphics.Bitmap +import android.media.ThumbnailUtils +import android.provider.MediaStore +import im.vector.matrix.android.api.session.content.ContentAttachmentData +import java.io.ByteArrayOutputStream +import java.io.File + +internal object ThumbnailExtractor { + + class ThumbnailData( + val width: Int, + val height: Int, + val size: Long, + val bytes: ByteArray, + val mimeType: String + ) + + fun extractThumbnail(attachment: ContentAttachmentData): ThumbnailData? { + val file = File(attachment.path) + if (!file.exists() || !file.isFile) { + return null + } + return if (attachment.type == ContentAttachmentData.Type.VIDEO) { + extractVideoThumbnail(attachment) + } else { + null + } + } + + private fun extractVideoThumbnail(attachment: ContentAttachmentData): ThumbnailData? { + val thumbnail = ThumbnailUtils.createVideoThumbnail(attachment.path, MediaStore.Video.Thumbnails.MINI_KIND) + val outputStream = ByteArrayOutputStream() + thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) + val thumbnailWidth = thumbnail.width + val thumbnailHeight = thumbnail.height + val thumbnailSize = outputStream.size() + val thumbnailData = ThumbnailData( + width = thumbnailWidth, + height = thumbnailHeight, + size = thumbnailSize.toLong(), + bytes = outputStream.toByteArray(), + mimeType = "image/jpeg" + ) + thumbnail.recycle() + outputStream.reset() + return thumbnailData + } + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index f864e725ff..4eeb124de9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -21,20 +21,29 @@ import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.content.ContentAttachmentData +import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageImageContent +import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.internal.di.MatrixKoinComponent +import im.vector.matrix.android.internal.network.ProgressRequestBody import im.vector.matrix.android.internal.session.room.send.SendEventWorker import im.vector.matrix.android.internal.util.WorkerParamsFactory import org.koin.standalone.inject +import timber.log.Timber +import java.io.File + internal class UploadContentWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params), MatrixKoinComponent { - private val mediaUploader by inject() + private val fileUploader by inject() + private val contentUploadProgressTracker by inject() @JsonClass(generateAdapter = true) internal data class Params( @@ -47,28 +56,65 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) val params = WorkerParamsFactory.fromData(inputData) ?: return Result.failure() - if (params.event.eventId == null) { - return Result.failure() + val eventId = params.event.eventId ?: return Result.failure() + val attachment = params.attachment + + val thumbnailData = ThumbnailExtractor.extractThumbnail(params.attachment) + val attachmentFile = createAttachmentFile(attachment) ?: return Result.failure() + var uploadedThumbnailUrl: String? = null + + if (thumbnailData != null) { + fileUploader + .uploadByteArray(thumbnailData.bytes, "thumb_${attachment.name}", thumbnailData.mimeType) + .fold( + { Timber.e(it) }, + { uploadedThumbnailUrl = it.contentUri } + ) } - return mediaUploader - .uploadFile(params.event.eventId, params.attachment) - .fold({ handleFailure() }, { handleSuccess(params, it) }) + + val progressListener = object : ProgressRequestBody.Listener { + override fun onProgress(current: Long, total: Long) { + contentUploadProgressTracker.setProgress(eventId, current, total) + } + } + return fileUploader + .uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener) + .fold( + { handleFailure(params) }, + { handleSuccess(params, it.contentUri, uploadedThumbnailUrl) } + ) } - private fun handleFailure(): Result { - return Result.retry() + private fun createAttachmentFile(attachment: ContentAttachmentData): File? { + return try { + File(attachment.path) + } catch (e: Exception) { + Timber.e(e) + null + } } - private fun handleSuccess(params: Params, contentUploadResponse: ContentUploadResponse): Result { - val event = updateEvent(params.event, contentUploadResponse.contentUri) + private fun handleFailure(params: Params): Result { + contentUploadProgressTracker.setFailure(params.event.eventId!!) + return Result.failure() + } + + private fun handleSuccess(params: Params, + attachmentUrl: String, + thumbnailUrl: String?): Result { + contentUploadProgressTracker.setFailure(params.event.eventId!!) + val event = updateEvent(params.event, attachmentUrl, thumbnailUrl) val sendParams = SendEventWorker.Params(params.roomId, event) return Result.success(WorkerParamsFactory.toData(sendParams)) } - private fun updateEvent(event: Event, url: String): Event { + private fun updateEvent(event: Event, url: String, thumbnailUrl: String? = null): Event { val messageContent: MessageContent = event.content.toModel() ?: return event val updatedContent = when (messageContent) { is MessageImageContent -> messageContent.update(url) + is MessageVideoContent -> messageContent.update(url, thumbnailUrl) + is MessageFileContent -> messageContent.update(url) + is MessageAudioContent -> messageContent.update(url) else -> messageContent } return event.copy(content = updatedContent.toContent()) @@ -78,6 +124,18 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) return copy(url = url) } + private fun MessageVideoContent.update(url: String, thumbnailUrl: String?): MessageVideoContent { + return copy(url = url, info = info?.copy(thumbnailUrl = thumbnailUrl)) + } + + private fun MessageFileContent.update(url: String): MessageFileContent { + return copy(url = url) + } + + private fun MessageAudioContent.update(url: String): MessageAudioContent { + return copy(url = url) + } + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 146794e15d..24e513551e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -21,7 +21,7 @@ import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.internal.session.room.invite.InviteTask import im.vector.matrix.android.internal.session.room.members.DefaultRoomMembersService import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask -import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor +import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor import im.vector.matrix.android.internal.session.room.read.DefaultReadService import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask import im.vector.matrix.android.internal.session.room.send.DefaultSendService @@ -45,7 +45,7 @@ internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask, private val taskExecutor: TaskExecutor) { fun instantiate(roomId: String): Room { - val roomMemberExtractor = RoomMemberExtractor(roomId) + val roomMemberExtractor = SenderRoomMemberExtractor(roomId) val timelineEventFactory = TimelineEventFactory(roomMemberExtractor) val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask) val sendService = DefaultSendService(roomId, eventFactory, monarchy) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt index 6c6d78ed2e..40266cb75f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt @@ -48,6 +48,18 @@ internal class RoomMembers(private val realm: Realm, } } + fun isUniqueDisplayName(displayName: String?): Boolean { + if(displayName.isNullOrEmpty()){ + return true + } + return EventEntity + .where(realm, roomId, EventType.STATE_ROOM_MEMBER) + .contains(EventEntityFields.CONTENT, displayName) + .distinct(EventEntityFields.STATE_KEY) + .findAll() + .size == 1 + } + fun queryRoomMembersEvent(): RealmQuery { return EventEntity .where(realm, roomId, EventType.STATE_ROOM_MEMBER) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/SenderRoomMemberExtractor.kt similarity index 56% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/SenderRoomMemberExtractor.kt index f2ec6a056f..d07175d40a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/SenderRoomMemberExtractor.kt @@ -20,48 +20,45 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.internal.database.mapper.ContentMapper +import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.query.findIncludingEvent import im.vector.matrix.android.internal.database.query.next import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where -import io.realm.Realm +import io.realm.RealmList import io.realm.RealmQuery -internal class RoomMemberExtractor(private val roomId: String) { - - private val cached = HashMap() +internal class SenderRoomMemberExtractor(private val roomId: String) { fun extractFrom(event: EventEntity): RoomMember? { val sender = event.sender ?: return null - val cacheKey = sender + event.stateIndex - if (cached.containsKey(cacheKey)) { - return cached[cacheKey] - } // If the event is unlinked we want to fetch unlinked state events val unlinked = event.isUnlinked - // When stateIndex is negative, we try to get the next stateEvent prevContent() - // If prevContent is null we fallback to the Int.MIN state events content() - val content = if (event.stateIndex <= 0) { - baseQuery(event.realm, roomId, sender, unlinked).next(from = event.stateIndex)?.prevContent - ?: baseQuery(event.realm, roomId, sender, unlinked).prev(since = event.stateIndex)?.content - } else { - baseQuery(event.realm, roomId, sender, unlinked).prev(since = event.stateIndex)?.content + val roomEntity = RoomEntity.where(event.realm, roomId = roomId).findFirst() ?: return null + val chunkEntity = ChunkEntity.findIncludingEvent(event.realm, event.eventId) + val content = when { + chunkEntity == null -> null + event.stateIndex <= 0 -> baseQuery(chunkEntity.events, sender, unlinked).next(from = event.stateIndex)?.prevContent + else -> baseQuery(chunkEntity.events, sender, unlinked).prev(since = event.stateIndex)?.content } - val roomMember: RoomMember? = ContentMapper.map(content).toModel() - cached[cacheKey] = roomMember - return roomMember + + val fallbackContent = content + ?: baseQuery(roomEntity.untimelinedStateEvents, sender, unlinked).prev(since = event.stateIndex)?.content + + return ContentMapper.map(fallbackContent).toModel() } - private fun baseQuery(realm: Realm, - roomId: String, + private fun baseQuery(list: RealmList, sender: String, isUnlinked: Boolean): RealmQuery { - - val filterMode = if (isUnlinked) EventEntity.LinkFilterMode.UNLINKED_ONLY else EventEntity.LinkFilterMode.LINKED_ONLY - return EventEntity - .where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER, linkFilterMode = filterMode) + return list + .where() .equalTo(EventEntityFields.STATE_KEY, sender) + .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER) + .equalTo(EventEntityFields.IS_UNLINKED, isUnlinked) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 56051503fc..778a49eca4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.session.room.send +import android.media.MediaMetadataRetriever import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event @@ -30,7 +31,9 @@ import im.vector.matrix.android.api.session.room.model.message.MessageImageConte import im.vector.matrix.android.api.session.room.model.message.MessageTextContent 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.ThumbnailInfo import im.vector.matrix.android.api.session.room.model.message.VideoInfo +import im.vector.matrix.android.internal.session.content.ThumbnailExtractor internal class LocalEchoEventFactory(private val credentials: Credentials) { @@ -53,7 +56,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) { type = MessageType.MSGTYPE_IMAGE, body = attachment.name ?: "image", info = ImageInfo( - mimeType = attachment.mimeType ?: "image/png", + mimeType = attachment.mimeType, width = attachment.width?.toInt() ?: 0, height = attachment.height?.toInt() ?: 0, size = attachment.size.toInt() @@ -64,15 +67,35 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) { } private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event { + val mediaDataRetriever = MediaMetadataRetriever() + mediaDataRetriever.setDataSource(attachment.path) + + // Use frame to calculate height and width as we are sure to get the right ones + val firstFrame = mediaDataRetriever.frameAtTime + val height = firstFrame.height + val width = firstFrame.width + mediaDataRetriever.release() + + val thumbnailInfo = ThumbnailExtractor.extractThumbnail(attachment)?.let { + ThumbnailInfo( + width = it.width, + height = it.height, + size = it.size, + mimeType = it.mimeType + ) + } val content = MessageVideoContent( type = MessageType.MSGTYPE_VIDEO, body = attachment.name ?: "video", info = VideoInfo( - mimeType = attachment.mimeType ?: "video/mpeg", - width = attachment.width?.toInt() ?: 0, - height = attachment.height?.toInt() ?: 0, + mimeType = attachment.mimeType, + width = width, + height = height, size = attachment.size, - duration = attachment.duration?.toInt() ?: 0 + duration = attachment.duration?.toInt() ?: 0, + // Glide will be able to use the local path and extract a thumbnail. + thumbnailUrl = attachment.path, + thumbnailInfo = thumbnailInfo ), url = attachment.path ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 89a8307b7f..58274288e3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -27,14 +27,24 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.api.util.addTo -import im.vector.matrix.android.internal.database.model.* +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.ChunkEntityFields +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.findIncludingEvent import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.Debouncer -import io.realm.* +import io.realm.OrderedCollectionChangeSet +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.Sort import timber.log.Timber import java.util.* import java.util.concurrent.atomic.AtomicBoolean @@ -87,9 +97,16 @@ internal class DefaultTimeline( private val eventsChangeListener = OrderedRealmCollectionChangeListener> { _, changeSet -> - if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) { + if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL ) { handleInitialLoad() } else { + // If changeSet has deletion we are having a gap, so we clear everything + if(changeSet.deletionRanges.isNotEmpty()){ + prevDisplayIndex = DISPLAY_INDEX_UNKNOWN + nextDisplayIndex = DISPLAY_INDEX_UNKNOWN + builtEvents.clear() + timelineEventFactory.clear() + } changeSet.insertionRanges.forEach { range -> val (startDisplayIndex, direction) = if (range.startIndex == 0) { Pair(liveEvents[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS) @@ -108,6 +125,7 @@ internal class DefaultTimeline( buildTimelineEvents(startDisplayIndex, direction, range.length.toLong()) postSnapshot() } + } } } @@ -298,9 +316,9 @@ internal class DefaultTimeline( private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { val token = getTokenLive(direction) ?: return val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) + from = token, + direction = direction.toPaginationDirection(), + limit = limit) Timber.v("Should fetch $limit items $direction") paginationTask.configureWith(params) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt index 20c0ebee68..28cd2a9d96 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt @@ -19,19 +19,36 @@ package im.vector.matrix.android.internal.session.room.timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor +import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor -internal class TimelineEventFactory(private val roomMemberExtractor: RoomMemberExtractor) { +internal class TimelineEventFactory(private val roomMemberExtractor: SenderRoomMemberExtractor) { + + private val cached = mutableMapOf() fun create(eventEntity: EventEntity): TimelineEvent { - val roomMember = roomMemberExtractor.extractFrom(eventEntity) + val sender = eventEntity.sender + val cacheKey = sender + eventEntity.stateIndex + val senderData = cached.getOrPut(cacheKey) { + val senderRoomMember = roomMemberExtractor.extractFrom(eventEntity) + SenderData(senderRoomMember?.displayName, senderRoomMember?.avatarUrl) + } return TimelineEvent( eventEntity.asDomain(), eventEntity.localId, eventEntity.displayIndex, - roomMember, + senderData.senderName, + senderData.senderAvatar, eventEntity.sendState ) } + fun clear(){ + cached.clear() + } + + private data class SenderData( + val senderName: String?, + val senderAvatar: String? + ) + } \ No newline at end of file diff --git a/vector/build.gradle b/vector/build.gradle index 691090f07f..bdf958030b 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -187,6 +187,7 @@ dependencies { implementation "com.github.piasy:GlideImageViewFactory:$big_image_viewer_version" implementation "com.github.bumptech.glide:glide:$glide_version" kapt "com.github.bumptech.glide:compiler:$glide_version" + implementation 'com.danikula:videocache:2.7.1' // Badge for compatibility implementation 'me.leolin:ShortcutBadger:1.1.2@aar' diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index bea4280eef..7aac94fa4f 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -29,7 +29,7 @@ - + @@ -37,6 +37,7 @@ android:name=".features.settings.VectorSettingsActivity" android:label="@string/title_activity_settings" android:windowSoftInputMode="adjustResize" /> + >() { data.forEach { user -> autocompleteUserItem { id(user.userId) + userId(user.userId) name(user.displayName) avatarUrl(user.avatarUrl) clickListener { _ -> diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt index 6678bff543..576947253e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt @@ -29,18 +29,15 @@ import im.vector.riotredesign.features.home.AvatarRenderer @EpoxyModelClass(layout = R.layout.item_autocomplete_user) abstract class AutocompleteUserItem : VectorEpoxyModel() { - @EpoxyAttribute - var name: String? = null - @EpoxyAttribute - var avatarUrl: String? = null - @EpoxyAttribute - var clickListener: View.OnClickListener? = null + @EpoxyAttribute var name: String? = null + @EpoxyAttribute var userId: String = "" + @EpoxyAttribute var avatarUrl: String? = null + @EpoxyAttribute var clickListener: View.OnClickListener? = null override fun bind(holder: Holder) { holder.view.setOnClickListener(clickListener) - holder.nameView.text = name - AvatarRenderer.render(avatarUrl, name, holder.avatarImageView) + AvatarRenderer.render(avatarUrl, userId, name, holder.avatarImageView) } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt index b64cadb18d..70974710db 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt @@ -29,7 +29,6 @@ import com.bumptech.glide.request.target.Target import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.session.content.ContentUrlResolver -import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.riotredesign.R import im.vector.riotredesign.core.glide.GlideApp @@ -44,39 +43,41 @@ object AvatarRenderer { private const val THUMBNAIL_SIZE = 250 - @UiThread - fun render(roomMember: RoomMember, imageView: ImageView) { - render(roomMember.avatarUrl, roomMember.displayName, imageView) - } + private val AVATAR_COLOR_LIST = listOf( + R.color.avatar_color_1, + R.color.avatar_color_2, + R.color.avatar_color_3 + ) @UiThread fun render(roomSummary: RoomSummary, imageView: ImageView) { - render(roomSummary.avatarUrl, roomSummary.displayName, imageView) + render(roomSummary.avatarUrl, roomSummary.roomId, roomSummary.displayName, imageView) } @UiThread - fun render(avatarUrl: String?, name: String?, imageView: ImageView) { - render(imageView.context, GlideApp.with(imageView), avatarUrl, name, DrawableImageViewTarget(imageView)) + fun render(avatarUrl: String?, identifier: String, name: String?, imageView: ImageView) { + render(imageView.context, GlideApp.with(imageView), avatarUrl, identifier, name, DrawableImageViewTarget(imageView)) } @UiThread fun render(context: Context, glideRequest: GlideRequests, avatarUrl: String?, + identifier: String, name: String?, target: Target) { if (name.isNullOrEmpty()) { return } - val placeholder = getPlaceholderDrawable(context, name) + val placeholder = getPlaceholderDrawable(context, identifier, name) buildGlideRequest(glideRequest, avatarUrl) .placeholder(placeholder) .into(target) } @AnyThread - fun getPlaceholderDrawable(context: Context, text: String): Drawable { - val avatarColor = ContextCompat.getColor(context, R.color.pale_teal) + fun getPlaceholderDrawable(context: Context, identifier: String, text: String): Drawable { + val avatarColor = ContextCompat.getColor(context, getAvatarColor(identifier)) return if (text.isEmpty()) { TextDrawable.builder().buildRound("", avatarColor) } else { @@ -87,9 +88,21 @@ object AvatarRenderer { } } - // PRIVATE API ********************************************************************************* + + private fun getAvatarColor(text: String? = null): Int { + var colorIndex: Long = 0 + if (!text.isNullOrEmpty()) { + var sum: Long = 0 + for (i in 0 until text.length) { + sum += text[i].toLong() + } + colorIndex = sum % AVATAR_COLOR_LIST.size + } + return AVATAR_COLOR_LIST[colorIndex.toInt()] + } + private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest { val resolvedUrl = Matrix.getInstance().currentSession!!.contentUrlResolver() .resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt index 0fc0c9d4d5..fe40b162c4 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt @@ -35,6 +35,7 @@ class GroupSummaryController : TypedEpoxyController() { val isSelected = groupSummary.groupId == selected?.groupId groupSummaryItem { id(groupSummary.groupId) + groupId(groupSummary.groupId) groupName(groupSummary.displayName) selected(isSelected) avatarUrl(groupSummary.avatarUrl) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryItem.kt index 3acf7f0a76..eef0e8659a 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryItem.kt @@ -29,6 +29,7 @@ import im.vector.riotredesign.features.home.AvatarRenderer abstract class GroupSummaryItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var groupName: CharSequence + @EpoxyAttribute lateinit var groupId: String @EpoxyAttribute var avatarUrl: String? = null @EpoxyAttribute var selected: Boolean = false @EpoxyAttribute var listener: (() -> Unit)? = null @@ -37,7 +38,7 @@ abstract class GroupSummaryItem : VectorEpoxyModel() { super.bind(holder) holder.rootView.isSelected = selected holder.rootView.setOnClickListener { listener?.invoke() } - AvatarRenderer.render(avatarUrl, groupName.toString(), holder.avatarImageView) + AvatarRenderer.render(avatarUrl, groupId, groupName.toString(), holder.avatarImageView) } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 4abb879386..094cf22572 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -37,6 +37,10 @@ import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.CharPolicy import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent +import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.user.model.User import im.vector.riotredesign.R @@ -60,8 +64,10 @@ import im.vector.riotredesign.features.home.room.detail.composer.TextComposerVie import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener import im.vector.riotredesign.features.html.PillImageSpan -import im.vector.riotredesign.features.media.MediaContentRenderer -import im.vector.riotredesign.features.media.MediaViewerActivity +import im.vector.riotredesign.features.media.ImageContentRenderer +import im.vector.riotredesign.features.media.ImageMediaViewerActivity +import im.vector.riotredesign.features.media.VideoContentRenderer +import im.vector.riotredesign.features.media.VideoMediaViewerActivity import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_detail.* import org.koin.android.ext.android.inject @@ -367,7 +373,7 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac .show() } -// TimelineEventController.Callback ************************************************************ + // TimelineEventController.Callback ************************************************************ override fun onUrlClicked(url: String) { homePermalinkHandler.launch(url) @@ -377,12 +383,25 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event)) } - override fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View) { - val intent = MediaViewerActivity.newIntent(vectorBaseActivity, mediaData) + override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) { + val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData) startActivity(intent) } -// AutocompleteUserPresenter.Callback + override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) { + val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData) + startActivity(intent) + } + + override fun onFileMessageClicked(messageFileContent: MessageFileContent) { + vectorBaseActivity.notImplemented() + } + + override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) { + vectorBaseActivity.notImplemented() + } + + // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { textComposerViewModel.process(TextComposerActions.QueryUsers(query)) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index 618c15c210..05b20b2f8b 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -24,16 +24,32 @@ import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyModel -import com.airbnb.epoxy.VisibilityState +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent +import im.vector.matrix.android.api.session.room.model.message.MessageImageContent +import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.core.epoxy.LoadingItemModel_ -import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory -import im.vector.riotredesign.features.home.room.detail.timeline.helper.* +import im.vector.riotredesign.features.home.room.detail.timeline.helper.RoomMemberEventHelper +import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineAsyncHelper +import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter +import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback +import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener +import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider +import im.vector.riotredesign.features.home.room.detail.timeline.helper.canBeMerged +import im.vector.riotredesign.features.home.room.detail.timeline.helper.nextDisplayableEvent +import im.vector.riotredesign.features.home.room.detail.timeline.helper.prevSameTypeEvents +import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem_ -import im.vector.riotredesign.features.media.MediaContentRenderer +import im.vector.riotredesign.features.home.room.detail.timeline.item.MergedHeaderItem +import im.vector.riotredesign.features.media.ImageContentRenderer +import im.vector.riotredesign.features.media.VideoContentRenderer +import org.threeten.bp.LocalDateTime class TimelineEventController(private val dateFormatter: TimelineDateFormatter, private val timelineItemFactory: TimelineItemFactory, @@ -44,10 +60,16 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, interface Callback { fun onEventVisible(event: TimelineEvent) fun onUrlClicked(url: String) - fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View) + fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) + fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) + fun onFileMessageClicked(messageFileContent: MessageFileContent) + fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) } - private val modelCache = arrayListOf>>() + private val collapsedEventIds = linkedSetOf() + private val mergeItemCollapseStates = HashMap() + private val modelCache = arrayListOf() + private var currentSnapshot: List = emptyList() private var inSubmitList: Boolean = false private var timeline: Timeline? = null @@ -60,7 +82,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, override fun onChanged(position: Int, count: Int, payload: Any?) { assertUpdateCallbacksAllowed() (position until (position + count)).forEach { - modelCache[it] = emptyList() + modelCache[it] = null } requestModelBuild() } @@ -76,11 +98,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, @Synchronized override fun onInserted(position: Int, count: Int) { assertUpdateCallbacksAllowed() - if (modelCache.isNotEmpty() && position == modelCache.size) { - modelCache[position - 1] = emptyList() - } (0 until count).forEach { - modelCache.add(position, emptyList()) + modelCache.add(position, null) } requestModelBuild() } @@ -116,7 +135,6 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, .id("forward_loading_item") .addWhen(Timeline.Direction.FORWARDS) - val timelineModels = getModels() add(timelineModels) @@ -149,53 +167,110 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, @Synchronized private fun getModels(): List> { (0 until modelCache.size).forEach { position -> - if (modelCache[position].isEmpty()) { + // Should be build if not cached or if cached but contains mergedHeader or formattedDay + // We then are sure we always have items up to date. + if (modelCache[position] == null + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { modelCache[position] = buildItemModels(position, currentSnapshot) } } - return modelCache.flatten() + return modelCache + .map { + val eventModel = if (it == null || collapsedEventIds.contains(it.localId)) { + null + } else { + it.eventModel + } + listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel) + } + .flatten() + .filterNotNull() } - private fun buildItemModels(currentPosition: Int, items: List): List> { - val epoxyModels = ArrayList>() + + private fun buildItemModels(currentPosition: Int, items: List): CacheItemData { val event = items[currentPosition] val nextEvent = items.nextDisplayableEvent(currentPosition) - val date = event.root.localDateTime() val nextDate = nextEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() - timelineItemFactory.create(event, nextEvent, callback).also { + val eventModel = timelineItemFactory.create(event, nextEvent, callback).also { it.id(event.localId) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) - epoxyModels.add(it) } - if (addDaySeparator) { + val mergedHeaderModel = buildMergedHeaderItem(event, nextEvent, items, addDaySeparator, currentPosition) + val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) + + return CacheItemData(event.localId, eventModel, mergedHeaderModel, daySeparatorItem) + } + + private fun buildDaySeparatorItem(addDaySeparator: Boolean, date: LocalDateTime): DaySeparatorItem? { + return if (addDaySeparator) { val formattedDay = dateFormatter.formatMessageDay(date) - val daySeparatorItem = DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay) - epoxyModels.add(daySeparatorItem) + DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay) + } else { + null + } + } + + private fun buildMergedHeaderItem(event: TimelineEvent, + nextEvent: TimelineEvent?, + items: List, + addDaySeparator: Boolean, + currentPosition: Int): MergedHeaderItem? { + return if (!event.canBeMerged() || (nextEvent?.root?.type == event.root.type && !addDaySeparator)) { + null + } else { + val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2) + if (prevSameTypeEvents.isEmpty()) { + null + } else { + val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed() + val mergedData = mergedEvents.map { mergedEvent -> + val eventContent: RoomMember? = mergedEvent.root.content.toModel() + val prevEventContent: RoomMember? = mergedEvent.root.prevContent.toModel() + val senderAvatar = RoomMemberEventHelper.senderAvatar(eventContent, prevEventContent, mergedEvent) + val senderName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, mergedEvent) + MergedHeaderItem.Data( + userId = mergedEvent.root.sender ?: "", + avatarUrl = senderAvatar, + memberName = senderName ?: "", + eventId = mergedEvent.localId + ) + } + val mergedEventIds = mergedEvents.map { it.localId } + // We try to find if one of the item id were used as mergeItemCollapseStates key + // => handle case where paginating from mergeable events and we get more + val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() + val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) + ?: true + val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } + if (isCollapsed) { + collapsedEventIds.addAll(mergedEventIds) + } else { + collapsedEventIds.removeAll(mergedEventIds) + } + val mergeId = mergedEventIds.joinToString(separator = "_") { it } + MergedHeaderItem(isCollapsed, mergeId, mergedData) { + mergeItemCollapseStates[event.localId] = it + requestModelBuild() + } + } } - return epoxyModels } private fun LoadingItemModel_.addWhen(direction: Timeline.Direction) { - val shouldAdd = timeline?.let { - it.hasMoreToLoad(direction) - } ?: false + val shouldAdd = timeline?.hasMoreToLoad(direction) ?: false addIf(shouldAdd, this@TimelineEventController) } } -private class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?, - private val event: TimelineEvent) - : VectorEpoxyModel.OnVisibilityStateChangedListener { - - override fun onVisibilityStateChanged(visibilityState: Int) { - if (visibilityState == VisibilityState.VISIBLE) { - callback?.onEventVisible(event) - } - } - - -} \ No newline at end of file +private data class CacheItemData( + val localId: String, + val eventModel: EpoxyModel<*>? = null, + val mergedHeaderModel: MergedHeaderItem? = null, + val formattedDayModel: DaySeparatorItem? = null +) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/CallItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/CallItemFactory.kt index e83183ef24..42f6ba0e5d 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/CallItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/CallItemFactory.kt @@ -30,27 +30,26 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem class CallItemFactory(private val stringProvider: StringProvider) { fun create(event: TimelineEvent): NoticeItem? { - val roomMember = event.roomMember ?: return null - val text = buildNoticeText(event.root, roomMember) ?: return null + val text = buildNoticeText(event.root, event.senderName) ?: return null return NoticeItem_() .noticeText(text) - .avatarUrl(roomMember.avatarUrl) - .memberName(roomMember.displayName) + .avatarUrl(event.senderAvatar) + .memberName(event.senderName) } - private fun buildNoticeText(event: Event, roomMember: RoomMember): CharSequence? { + private fun buildNoticeText(event: Event, senderName: String?): CharSequence? { return when { EventType.CALL_INVITE == event.type -> { val content = event.content.toModel() ?: return null val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO return if (isVideoCall) { - stringProvider.getString(R.string.notice_placed_video_call, roomMember.displayName) + stringProvider.getString(R.string.notice_placed_video_call, senderName) } else { - stringProvider.getString(R.string.notice_placed_voice_call, roomMember.displayName) + stringProvider.getString(R.string.notice_placed_voice_call, senderName) } } - EventType.CALL_ANSWER == event.type -> stringProvider.getString(R.string.notice_answered_call, roomMember.displayName) - EventType.CALL_HANGUP == event.type -> stringProvider.getString(R.string.notice_ended_call, roomMember.displayName) + EventType.CALL_ANSWER == event.type -> stringProvider.getString(R.string.notice_answered_call, senderName) + EventType.CALL_HANGUP == event.type -> stringProvider.getString(R.string.notice_ended_call, senderName) else -> null } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt index f3596c4f15..f17cc29551 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -18,16 +18,19 @@ package im.vector.riotredesign.features.home.room.detail.timeline.factory import android.text.Spannable import android.text.SpannableStringBuilder +import androidx.annotation.ColorRes import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageImageContent import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent -import im.vector.matrix.android.api.session.room.send.SendState +import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.R import im.vector.riotredesign.core.epoxy.VectorEpoxyModel @@ -39,13 +42,16 @@ import im.vector.riotredesign.features.home.room.detail.timeline.helper.Timeline import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultItem import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultItem_ -import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageItem -import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageItem_ +import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageFileItem +import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageFileItem_ +import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageVideoItem +import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageVideoItem_ import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.riotredesign.features.html.EventHtmlRenderer -import im.vector.riotredesign.features.media.MediaContentRenderer +import im.vector.riotredesign.features.media.ImageContentRenderer +import im.vector.riotredesign.features.media.VideoContentRenderer import me.gujun.android.span.span class MessageItemFactory(private val colorProvider: ColorProvider, @@ -59,8 +65,6 @@ class MessageItemFactory(private val colorProvider: ColorProvider, ): VectorEpoxyModel<*>? { val eventId = event.root.eventId ?: return null - val roomMember = event.roomMember - val nextRoomMember = nextEvent?.roomMember val date = event.root.localDateTime() val nextDate = nextEvent?.root?.localDateTime() @@ -69,56 +73,113 @@ class MessageItemFactory(private val colorProvider: ColorProvider, ?: false val showInformation = addDaySeparator - || nextRoomMember != roomMember + || event.senderAvatar != nextEvent?.senderAvatar + || event.senderName != nextEvent?.senderName || nextEvent?.root?.type != EventType.MESSAGE || isNextMessageReceivedMoreThanOneHourAgo val messageContent: MessageContent = event.root.content.toModel() ?: return null val time = timelineDateFormatter.formatMessageHour(date) - val avatarUrl = roomMember?.avatarUrl - val memberName = roomMember?.displayName ?: event.root.sender - val informationData = MessageInformationData(time, avatarUrl, memberName, showInformation) + val avatarUrl = event.senderAvatar + val memberName = event.senderName ?: event.root.sender ?: "" + val formattedMemberName = span(memberName) { + textColor = colorProvider.getColor(getColorFor(event.root.sender ?: "")) + } + val informationData = MessageInformationData(eventId = eventId, + senderId = event.root.sender ?: "", + sendState = event.sendState, + time = time, + avatarUrl = avatarUrl, + memberName = formattedMemberName, + showInformation = showInformation) return when (messageContent) { is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback) - is MessageTextContent -> buildTextMessageItem(event.sendState, messageContent, informationData, callback) - is MessageImageContent -> buildImageMessageItem(eventId, messageContent, informationData, callback) - is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback) + is MessageTextContent -> buildTextMessageItem(messageContent, informationData, callback) + is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback) + is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback) + is MessageFileContent -> buildFileMessageItem(messageContent, informationData, callback) + is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, callback) else -> buildNotHandledMessageItem(messageContent) } } + private fun buildAudioMessageItem(messageContent: MessageAudioContent, + informationData: MessageInformationData, + callback: TimelineEventController.Callback?): MessageFileItem? { + return MessageFileItem_() + .informationData(informationData) + .filename(messageContent.body) + .iconRes(R.drawable.filetype_audio) + .clickListener { _ -> callback?.onAudioMessageClicked(messageContent) } + } + + private fun buildFileMessageItem(messageContent: MessageFileContent, + informationData: MessageInformationData, + callback: TimelineEventController.Callback?): MessageFileItem? { + return MessageFileItem_() + .informationData(informationData) + .filename(messageContent.body) + .iconRes(R.drawable.filetype_attachment) + .clickListener { _ -> callback?.onFileMessageClicked(messageContent) } + } + private fun buildNotHandledMessageItem(messageContent: MessageContent): DefaultItem? { val text = "${messageContent.type} message events are not yet handled" return DefaultItem_().text(text) } - private fun buildImageMessageItem(eventId: String, - messageContent: MessageImageContent, + private fun buildImageMessageItem(messageContent: MessageImageContent, informationData: MessageInformationData, - callback: TimelineEventController.Callback?): MessageImageItem? { + callback: TimelineEventController.Callback?): MessageImageVideoItem? { val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() - val data = MediaContentRenderer.Data( - messageContent.body, + val data = ImageContentRenderer.Data( + filename = messageContent.body, url = messageContent.url, height = messageContent.info?.height, maxHeight = maxHeight, width = messageContent.info?.width, maxWidth = maxWidth, - rotation = messageContent.info?.rotation, - orientation = messageContent.info?.orientation + orientation = messageContent.info?.orientation, + rotation = messageContent.info?.rotation ) - return MessageImageItem_() - .eventId(eventId) + return MessageImageVideoItem_() + .playable(messageContent.info?.mimeType == "image/gif") .informationData(informationData) .mediaData(data) - .clickListener { view -> callback?.onMediaClicked(data, view) } + .clickListener { view -> callback?.onImageMessageClicked(messageContent, data, view) } } - private fun buildTextMessageItem(sendState: SendState, - messageContent: MessageTextContent, + private fun buildVideoMessageItem(messageContent: MessageVideoContent, + informationData: MessageInformationData, + callback: TimelineEventController.Callback?): MessageImageVideoItem? { + + val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() + val thumbnailData = ImageContentRenderer.Data( + filename = messageContent.body, + url = messageContent.info?.thumbnailUrl, + height = messageContent.info?.height, + maxHeight = maxHeight, + width = messageContent.info?.width, + maxWidth = maxWidth + ) + + val videoData = VideoContentRenderer.Data( + filename = messageContent.body, + videoUrl = messageContent.url, + thumbnailMediaData = thumbnailData + ) + + return MessageImageVideoItem_() + .playable(true) + .informationData(informationData) + .mediaData(thumbnailData) + .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) } + } + + private fun buildTextMessageItem(messageContent: MessageTextContent, informationData: MessageInformationData, callback: TimelineEventController.Callback?): MessageTextItem? { @@ -126,15 +187,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider, htmlRenderer.render(it) } ?: messageContent.body - val textColor = if (sendState.isSent()) { - R.color.dark_grey - } else { - R.color.brown_grey - } - val formattedBody = span(bodyToUse) { - this.textColor = colorProvider.getColor(textColor) - } - val linkifiedBody = linkifyBody(formattedBody, callback) + val linkifiedBody = linkifyBody(bodyToUse, callback) return MessageTextItem_() .message(linkifiedBody) .informationData(informationData) @@ -181,4 +234,31 @@ class MessageItemFactory(private val colorProvider: ColorProvider, return spannable } + //Based on riot-web implementation + @ColorRes + private fun getColorFor(sender: String): Int { + var hash = 0 + var i = 0 + var chr: Char + if (sender.isEmpty()) { + return R.color.username_1 + } + while (i < sender.length) { + chr = sender[i] + hash = (hash shl 5) - hash + chr.toInt() + hash = hash or 0 + i++ + } + val cI = Math.abs(hash) % 8 + 1 + return when (cI) { + 1 -> R.color.username_1 + 2 -> R.color.username_2 + 3 -> R.color.username_3 + 4 -> R.color.username_4 + 5 -> R.color.username_5 + 6 -> R.color.username_6 + 7 -> R.color.username_7 + else -> R.color.username_8 + } + } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomHistoryVisibilityItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomHistoryVisibilityItemFactory.kt index cd87e7bd05..3a3a91f1d2 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomHistoryVisibilityItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomHistoryVisibilityItemFactory.kt @@ -31,15 +31,14 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem class RoomHistoryVisibilityItemFactory(private val stringProvider: StringProvider) { fun create(event: TimelineEvent): NoticeItem? { - val roomMember = event.roomMember ?: return null - val noticeText = buildNoticeText(event.root, roomMember) ?: return null + val noticeText = buildNoticeText(event.root, event.senderName) ?: return null return NoticeItem_() .noticeText(noticeText) - .avatarUrl(roomMember.avatarUrl) - .memberName(roomMember.displayName) + .avatarUrl(event.senderAvatar) + .memberName(event.senderName) } - private fun buildNoticeText(event: Event, roomMember: RoomMember): CharSequence? { + private fun buildNoticeText(event: Event, senderName: String?): CharSequence? { val content = event.content.toModel() ?: return null val formattedVisibility = when (content.historyVisibility) { RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared) @@ -47,7 +46,7 @@ class RoomHistoryVisibilityItemFactory(private val stringProvider: StringProvide RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined) RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable) } - return stringProvider.getString(R.string.notice_made_future_room_visibility, roomMember.displayName, formattedVisibility) + return stringProvider.getString(R.string.notice_made_future_room_visibility, senderName, formattedVisibility) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt index 46c036ee9d..77e5920cb7 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.R import im.vector.riotredesign.core.resources.StringProvider +import im.vector.riotredesign.features.home.room.detail.timeline.helper.RoomMemberEventHelper import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_ @@ -31,17 +32,20 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem class RoomMemberItemFactory(private val stringProvider: StringProvider) { fun create(event: TimelineEvent): NoticeItem? { - val roomMember = event.roomMember ?: return null - val noticeText = buildRoomMemberNotice(event) ?: return null - return NoticeItem_() - .noticeText(noticeText) - .avatarUrl(roomMember.avatarUrl) - .memberName(roomMember.displayName) - } - - private fun buildRoomMemberNotice(event: TimelineEvent): String? { val eventContent: RoomMember? = event.root.content.toModel() val prevEventContent: RoomMember? = event.root.prevContent.toModel() + val noticeText = buildRoomMemberNotice(event, eventContent, prevEventContent) ?: return null + val senderAvatar = RoomMemberEventHelper.senderAvatar(eventContent, prevEventContent, event) + val senderName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, event) + + return NoticeItem_() + .userId(event.root.sender ?: "") + .noticeText(noticeText) + .avatarUrl(senderAvatar) + .memberName(senderName) + } + + private fun buildRoomMemberNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? { val isMembershipEvent = prevEventContent?.membership != eventContent?.membership return if (isMembershipEvent) { buildMembershipNotice(event, eventContent, prevEventContent) @@ -57,11 +61,11 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) { val displayNameText = when { prevEventContent?.displayName.isNullOrEmpty() -> stringProvider.getString(R.string.notice_display_name_set, event.root.sender, eventContent?.displayName) - eventContent?.displayName.isNullOrEmpty() -> + eventContent?.displayName.isNullOrEmpty() -> stringProvider.getString(R.string.notice_display_name_removed, event.root.sender, prevEventContent?.displayName) - else -> + else -> stringProvider.getString(R.string.notice_display_name_changed_from, - event.root.sender, prevEventContent?.displayName, eventContent?.displayName) + event.root.sender, prevEventContent?.displayName, eventContent?.displayName) } displayText.append(displayNameText) } @@ -71,7 +75,7 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) { displayText.append(" ") stringProvider.getString(R.string.notice_avatar_changed_too) } else { - stringProvider.getString(R.string.notice_avatar_url_changed, event.roomMember?.displayName) + stringProvider.getString(R.string.notice_avatar_url_changed, event.senderName) } displayText.append(displayAvatarText) } @@ -79,33 +83,34 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) { } private fun buildMembershipNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? { - val senderDisplayName = event.roomMember?.displayName ?: return null + val senderDisplayName = event.senderName ?: event.root.sender val targetDisplayName = eventContent?.displayName ?: event.root.sender return when { Membership.INVITE == eventContent?.membership -> { // TODO get userId - val selfUserId: String = "" + val selfUserId = "" when { - eventContent.thirdPartyInvite != null -> + eventContent.thirdPartyInvite != null -> stringProvider.getString(R.string.notice_room_third_party_registered_invite, - targetDisplayName, eventContent.thirdPartyInvite?.displayName) + targetDisplayName, eventContent.thirdPartyInvite?.displayName) TextUtils.equals(event.root.stateKey, selfUserId) -> stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName) - event.root.stateKey.isNullOrEmpty() -> + event.root.stateKey.isNullOrEmpty() -> stringProvider.getString(R.string.notice_room_invite_no_invitee, senderDisplayName) - else -> + else -> stringProvider.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName) } } - Membership.JOIN == eventContent?.membership -> + Membership.JOIN == eventContent?.membership -> stringProvider.getString(R.string.notice_room_join, senderDisplayName) - Membership.LEAVE == eventContent?.membership -> + Membership.LEAVE == eventContent?.membership -> // 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked return if (TextUtils.equals(event.root.sender, event.root.stateKey)) { if (prevEventContent?.membership == Membership.INVITE) { stringProvider.getString(R.string.notice_room_reject, senderDisplayName) } else { - stringProvider.getString(R.string.notice_room_leave, senderDisplayName) + val leftDisplayName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, event) + stringProvider.getString(R.string.notice_room_leave, leftDisplayName) } } else if (prevEventContent?.membership == Membership.INVITE) { stringProvider.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName) @@ -116,11 +121,11 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) { } else { null } - Membership.BAN == eventContent?.membership -> + Membership.BAN == eventContent?.membership -> stringProvider.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName) - Membership.KNOCK == eventContent?.membership -> + Membership.KNOCK == eventContent?.membership -> stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName) - else -> null + else -> null } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomNameItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomNameItemFactory.kt index 3904d7c738..be33c44ed0 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomNameItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomNameItemFactory.kt @@ -29,20 +29,16 @@ class RoomNameItemFactory(private val stringProvider: StringProvider) { fun create(event: TimelineEvent): NoticeItem? { - val content: RoomNameContent? = event.root.content.toModel() - val roomMember = event.roomMember - if (content == null || roomMember == null) { - return null - } + val content: RoomNameContent = event.root.content.toModel() ?: return null val text = if (!TextUtils.isEmpty(content.name)) { - stringProvider.getString(R.string.notice_room_name_changed, roomMember.displayName, content.name) + stringProvider.getString(R.string.notice_room_name_changed, event.senderName, content.name) } else { - stringProvider.getString(R.string.notice_room_name_removed, roomMember.displayName) + stringProvider.getString(R.string.notice_room_name_removed, event.senderName) } return NoticeItem_() .noticeText(text) - .avatarUrl(roomMember.avatarUrl) - .memberName(roomMember.displayName) + .avatarUrl(event.senderAvatar) + .memberName(event.senderName) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomTopicItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomTopicItemFactory.kt index 5aa31c9b61..34e558970b 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomTopicItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomTopicItemFactory.kt @@ -28,20 +28,16 @@ class RoomTopicItemFactory(private val stringProvider: StringProvider) { fun create(event: TimelineEvent): NoticeItem? { - val content: RoomTopicContent? = event.root.content.toModel() - val roomMember = event.roomMember - if (content == null || roomMember == null) { - return null - } + val content: RoomTopicContent = event.root.content.toModel() ?: return null val text = if (content.topic.isNullOrEmpty()) { - stringProvider.getString(R.string.notice_room_topic_removed, roomMember.displayName) + stringProvider.getString(R.string.notice_room_topic_removed, event.senderName) } else { - stringProvider.getString(R.string.notice_room_topic_changed, roomMember.displayName, content.topic) + stringProvider.getString(R.string.notice_room_topic_changed, event.senderName, content.topic) } return NoticeItem_() .noticeText(text) - .avatarUrl(roomMember.avatarUrl) - .memberName(roomMember.displayName) + .avatarUrl(event.senderAvatar) + .memberName(event.senderName) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 234490e4ab..d53544346e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -57,7 +57,7 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, } catch (e: Exception) { defaultItemFactory.create(event, e) } - return computedModel ?: EmptyItem_() + return (computedModel ?: EmptyItem_()) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt index d0d899ef50..8d395668ce 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt @@ -25,7 +25,7 @@ import android.widget.TextView import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.riotredesign.R -import im.vector.riotredesign.features.media.MediaContentRenderer +import im.vector.riotredesign.features.media.ImageContentRenderer import java.io.File object ContentUploadStateTrackerBinder { @@ -33,7 +33,7 @@ object ContentUploadStateTrackerBinder { private val updateListeners = mutableMapOf() fun bind(eventId: String, - mediaData: MediaContentRenderer.Data, + mediaData: ImageContentRenderer.Data, progressLayout: ViewGroup) { Matrix.getInstance().currentSession?.also { session -> @@ -56,7 +56,7 @@ object ContentUploadStateTrackerBinder { } private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, - private val mediaData: MediaContentRenderer.Data) : ContentUploadStateTracker.UpdateListener { + private val mediaData: ImageContentRenderer.Data) : ContentUploadStateTracker.UpdateListener { override fun onUpdate(state: ContentUploadStateTracker.State) { when (state) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt index c88bd7e9ef..d078e72c80 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt @@ -34,28 +34,18 @@ class EndlessRecyclerViewScrollListener(private val layoutManager: LinearLayoutM // This happens many times a second during a scroll, so be wary of the code you place here. // We are given a few useful parameters to help us work out if we need to load some more data, // but first we check if we are waiting for the previous load to finish. + override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() val totalItemCount = layoutManager.itemCount - // The minimum amount of items to have below your current scroll position - // before loading more. - // If the total item count is zero and the previous isn't, assume the - // list is invalidated and should be reset back to initial state - if (totalItemCount < previousTotalItemCount) { - previousTotalItemCount = totalItemCount - if (totalItemCount == 0) { - loadingForwards = true - loadingBackwards = true - } - } - // If it’s still loading, we check to see if the dataset count has + // We check to see if the dataset count has // changed, if so we conclude it has finished loading - if (totalItemCount > previousTotalItemCount) { + if (totalItemCount != previousTotalItemCount) { + previousTotalItemCount = totalItemCount loadingBackwards = false loadingForwards = false - previousTotalItemCount = totalItemCount } // If it isn’t currently loading, we check to see if we have reached // the visibleThreshold and need to reload more data. diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/RoomMemberEventHelper.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/RoomMemberEventHelper.kt new file mode 100644 index 0000000000..499f92f7f3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/RoomMemberEventHelper.kt @@ -0,0 +1,40 @@ +/* + * 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.riotredesign.features.home.room.detail.timeline.helper + +import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent + +object RoomMemberEventHelper { + + fun senderAvatar(eventContent: RoomMember?, prevEventContent: RoomMember?, event: TimelineEvent): String? { + return if (eventContent?.membership == Membership.LEAVE && eventContent.avatarUrl == null && prevEventContent?.avatarUrl != null) { + prevEventContent.avatarUrl + } else { + event.senderAvatar + } + } + + fun senderName(eventContent: RoomMember?, prevEventContent: RoomMember?, event: TimelineEvent): String? { + return if (eventContent?.membership == Membership.LEAVE && eventContent.displayName == null && prevEventContent?.displayName != null) { + prevEventContent.displayName + } else { + event.senderName + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 565f42548e..53d63f585a 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -18,6 +18,7 @@ package im.vector.riotredesign.features.home.room.detail.timeline.helper import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.riotredesign.core.extensions.localDateTime object TimelineDisplayableEvents { @@ -48,8 +49,48 @@ fun List.filterDisplayableEvents(): List { } } +fun TimelineEvent.canBeMerged(): Boolean { + return root.type == EventType.STATE_ROOM_MEMBER +} + +fun List.nextSameTypeEvents(index: Int, minSize: Int): List { + if (index >= size - 1) { + return emptyList() + } + val timelineEvent = this[index] + val nextSubList = subList(index + 1, size) + val indexOfNextDay = nextSubList.indexOfFirst { + val date = it.root.localDateTime() + val nextDate = timelineEvent.root.localDateTime() + date.toLocalDate() != nextDate.toLocalDate() + } + val nextSameDayEvents = if (indexOfNextDay == -1) { + nextSubList + } else { + nextSubList.subList(0, indexOfNextDay) + } + val indexOfFirstDifferentEventType = nextSameDayEvents.indexOfFirst { it.root.type != timelineEvent.root.type } + val sameTypeEvents = if (indexOfFirstDifferentEventType == -1) { + nextSameDayEvents + } else { + nextSameDayEvents.subList(0, indexOfFirstDifferentEventType) + } + if (sameTypeEvents.size < minSize) { + return emptyList() + } + return sameTypeEvents +} + +fun List.prevSameTypeEvents(index: Int, minSize: Int): List { + val prevSub = subList(0, index + 1) + return prevSub + .reversed() + .nextSameTypeEvents(0, minSize) + .reversed() +} + fun List.nextDisplayableEvent(index: Int): TimelineEvent? { - return if (index == size - 1) { + return if (index >= size - 1) { null } else { subList(index + 1, this.size).firstOrNull { it.isDisplayable() } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt new file mode 100644 index 0000000000..f103c18dde --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt @@ -0,0 +1,36 @@ +/* + * + * * 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.riotredesign.features.home.room.detail.timeline.helper + +import com.airbnb.epoxy.VisibilityState +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.riotredesign.core.epoxy.VectorEpoxyModel +import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController + +class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?, + private val event: TimelineEvent) + : VectorEpoxyModel.OnVisibilityStateChangedListener { + + override fun onVisibilityStateChanged(visibilityState: Int) { + if (visibilityState == VisibilityState.VISIBLE) { + callback?.onEventVisible(event) + } + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt index f4fe2e8dec..17c2ea4a91 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -35,7 +35,7 @@ abstract class AbsMessageItem : VectorEpoxyModel() holder.timeView.visibility = View.VISIBLE holder.timeView.text = informationData.time holder.memberNameView.text = informationData.memberName - AvatarRenderer.render(informationData.avatarUrl, informationData.memberName?.toString(), holder.avatarImageView) + AvatarRenderer.render(informationData.avatarUrl, informationData.senderId, informationData.memberName?.toString(), holder.avatarImageView) } else { holder.avatarImageView.visibility = View.GONE holder.memberNameView.visibility = View.GONE @@ -43,6 +43,11 @@ abstract class AbsMessageItem : VectorEpoxyModel() } } + protected fun View.renderSendState() { + isClickable = informationData.sendState.isSent() + alpha = if (informationData.sendState.isSent()) 1f else 0.5f + } + abstract class Holder : VectorEpoxyHolder() { abstract val avatarImageView: ImageView abstract val memberNameView: TextView diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MergedHeaderItem.kt new file mode 100644 index 0000000000..d5e05c11fa --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MergedHeaderItem.kt @@ -0,0 +1,94 @@ +/* + * + * * 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.riotredesign.features.home.room.detail.timeline.item + +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.children +import im.vector.riotredesign.R +import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder +import im.vector.riotredesign.core.epoxy.VectorEpoxyModel +import im.vector.riotredesign.features.home.AvatarRenderer + +data class MergedHeaderItem(private val isCollapsed: Boolean, + private val mergeId: String, + private val mergeData: List, + private val onCollapsedStateChanged: (Boolean) -> Unit +) : VectorEpoxyModel() { + + private val distinctMergeData = mergeData.distinctBy { it.userId } + + init { + id(mergeId) + } + + override fun getDefaultLayout(): Int { + return R.layout.item_timeline_event_merged_header + } + + override fun createNewHolder(): Holder { + return Holder() + } + + override fun bind(holder: Holder) { + super.bind(holder) + holder.expandView.setOnClickListener { + onCollapsedStateChanged(!isCollapsed) + } + if (isCollapsed) { + val summary = holder.expandView.resources.getQuantityString(R.plurals.membership_changes, mergeData.size, mergeData.size) + holder.summaryView.text = summary + holder.summaryView.visibility = View.VISIBLE + holder.avatarListView.visibility = View.VISIBLE + holder.avatarListView.children.forEachIndexed { index, view -> + val data = distinctMergeData.getOrNull(index) + if (data != null && view is ImageView) { + view.visibility = View.VISIBLE + AvatarRenderer.render(data.avatarUrl, data.userId, data.memberName, view) + } else { + view.visibility = View.GONE + } + } + holder.separatorView.visibility = View.GONE + holder.expandView.setText(R.string.merged_events_expand) + } else { + holder.avatarListView.visibility = View.INVISIBLE + holder.summaryView.visibility = View.GONE + holder.separatorView.visibility = View.VISIBLE + holder.expandView.setText(R.string.merged_events_collapse) + } + } + + data class Data( + val eventId: String, + val userId: String, + val memberName: String, + val avatarUrl: String? + ) + + class Holder : VectorEpoxyHolder() { + val expandView by bind(R.id.itemMergedExpandTextView) + val summaryView by bind(R.id.itemMergedSummaryTextView) + val separatorView by bind(R.id.itemMergedSeparatorView) + val avatarListView by bind(R.id.itemMergedAvatarListView) + + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageFileItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageFileItem.kt new file mode 100644 index 0000000000..edd3c779e6 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageFileItem.kt @@ -0,0 +1,57 @@ +/* + * 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.riotredesign.features.home.room.detail.timeline.item + +import android.graphics.Paint +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.DrawableRes +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotredesign.R + +@EpoxyModelClass(layout = R.layout.item_timeline_event_file_message) +abstract class MessageFileItem : AbsMessageItem() { + + @EpoxyAttribute var filename: CharSequence = "" + @EpoxyAttribute @DrawableRes var iconRes: Int = 0 + @EpoxyAttribute override lateinit var informationData: MessageInformationData + @EpoxyAttribute var clickListener: View.OnClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.fileLayout.renderSendState() + holder.filenameView.text = filename + holder.fileImageView.setImageResource(iconRes) + holder.filenameView.setOnClickListener(clickListener) + holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG) + } + + + class Holder : AbsMessageItem.Holder() { + override val avatarImageView by bind(R.id.messageAvatarImageView) + override val memberNameView by bind(R.id.messageMemberNameView) + override val timeView by bind(R.id.messageTimeView) + val fileLayout by bind(R.id.messageFileLayout) + val fileImageView by bind(R.id.messageFileImageView) + val filenameView by bind(R.id.messageFilenameView) + } + + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageVideoItem.kt similarity index 65% rename from vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageItem.kt rename to vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index 15e3d5ff08..27e464b16f 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -24,27 +24,27 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotredesign.R import im.vector.riotredesign.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder -import im.vector.riotredesign.features.media.MediaContentRenderer +import im.vector.riotredesign.features.media.ImageContentRenderer -@EpoxyModelClass(layout = R.layout.item_timeline_event_image_message) -abstract class MessageImageItem : AbsMessageItem() { +@EpoxyModelClass(layout = R.layout.item_timeline_event_image_video_message) +abstract class MessageImageVideoItem : AbsMessageItem() { - @EpoxyAttribute lateinit var mediaData: MediaContentRenderer.Data - @EpoxyAttribute lateinit var eventId: String + @EpoxyAttribute lateinit var mediaData: ImageContentRenderer.Data @EpoxyAttribute override lateinit var informationData: MessageInformationData + @EpoxyAttribute var playable: Boolean = false @EpoxyAttribute var clickListener: View.OnClickListener? = null override fun bind(holder: Holder) { super.bind(holder) - MediaContentRenderer.render(mediaData, MediaContentRenderer.Mode.THUMBNAIL, holder.imageView) - ContentUploadStateTrackerBinder.bind(eventId, mediaData, holder.progressLayout) + ImageContentRenderer.render(mediaData, ImageContentRenderer.Mode.THUMBNAIL, holder.imageView) + ContentUploadStateTrackerBinder.bind(informationData.eventId, mediaData, holder.progressLayout) holder.imageView.setOnClickListener(clickListener) - holder.imageView.isEnabled = !mediaData.isLocalFile() - holder.imageView.alpha = if (mediaData.isLocalFile()) 0.5f else 1f + holder.imageView.renderSendState() + holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE } override fun unbind(holder: Holder) { - ContentUploadStateTrackerBinder.unbind(eventId) + ContentUploadStateTrackerBinder.unbind(informationData.eventId) super.unbind(holder) } @@ -52,8 +52,9 @@ abstract class MessageImageItem : AbsMessageItem() { override val avatarImageView by bind(R.id.messageAvatarImageView) override val memberNameView by bind(R.id.messageMemberNameView) override val timeView by bind(R.id.messageTimeView) - val progressLayout by bind(R.id.messageImageUploadProgressLayout) - val imageView by bind(R.id.messageImageView) + val progressLayout by bind(R.id.messageMediaUploadProgressLayout) + val imageView by bind(R.id.messageThumbnailView) + val playContentView by bind(R.id.messageMediaPlayView) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt index f6ae60a93f..286f11a5b0 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -16,7 +16,12 @@ package im.vector.riotredesign.features.home.room.detail.timeline.item +import im.vector.matrix.android.api.session.room.send.SendState + data class MessageInformationData( + val eventId: String, + val senderId: String, + val sendState: SendState, val time: CharSequence? = null, val avatarUrl: String?, val memberName: CharSequence? = null, diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageTextItem.kt index 5c5ebbf933..45ba0ec147 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -45,6 +45,7 @@ abstract class MessageTextItem : AbsMessageItem() { TextViewCompat.getTextMetricsParams(holder.messageView), null) holder.messageView.setTextFuture(textFuture) + holder.messageView.renderSendState() findPillsAndProcess { it.bind(holder.messageView) } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/NoticeItem.kt index fe28892c83..821561531e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/NoticeItem.kt @@ -30,11 +30,12 @@ abstract class NoticeItem : VectorEpoxyModel() { @EpoxyAttribute var noticeText: CharSequence? = null @EpoxyAttribute var avatarUrl: String? = null + @EpoxyAttribute var userId: String = "" @EpoxyAttribute var memberName: CharSequence? = null override fun bind(holder: Holder) { holder.noticeTextView.text = noticeText - AvatarRenderer.render(avatarUrl, memberName?.toString(), holder.avatarImageView) + AvatarRenderer.render(avatarUrl, userId, memberName?.toString(), holder.avatarImageView) } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt index 8c0b742d33..24641da234 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt @@ -79,6 +79,7 @@ class RoomSummaryController(private val stringProvider: StringProvider roomSummaryItem { id(roomSummary.roomId) + roomId(roomSummary.roomId) roomName(roomSummary.displayName) avatarUrl(roomSummary.avatarUrl) selected(isSelected) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryItem.kt index 02a240d9f3..e68926101d 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryItem.kt @@ -31,6 +31,7 @@ import im.vector.riotredesign.features.home.AvatarRenderer abstract class RoomSummaryItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var roomName: CharSequence + @EpoxyAttribute lateinit var roomId: String @EpoxyAttribute var avatarUrl: String? = null @EpoxyAttribute var selected: Boolean = false @EpoxyAttribute var unreadCount: Int = 0 @@ -44,7 +45,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { holder.rootView.isChecked = selected holder.rootView.setOnClickListener { listener?.invoke() } holder.titleView.text = roomName - AvatarRenderer.render(avatarUrl, roomName.toString(), holder.avatarImageView) + AvatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView) } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt index 5ab1853109..e83e20e6b9 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt @@ -52,7 +52,7 @@ class PillImageSpan(private val glideRequests: GlideRequests, @UiThread fun bind(textView: TextView) { tv = WeakReference(textView) - AvatarRenderer.render(context, glideRequests, user?.avatarUrl, displayName, target) + AvatarRenderer.render(context, glideRequests, user?.avatarUrl, userId, displayName, target) } // ReplacementSpan ***************************************************************************** @@ -105,7 +105,7 @@ class PillImageSpan(private val glideRequests: GlideRequests, textStartPadding = textPadding setChipMinHeightResource(R.dimen.pill_min_height) setChipIconSizeResource(R.dimen.pill_avatar_size) - chipIcon = AvatarRenderer.getPlaceholderDrawable(context, displayName) + chipIcon = AvatarRenderer.getPlaceholderDrawable(context, userId, displayName) setBounds(0, 0, intrinsicWidth, intrinsicHeight) } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/media/MediaContentRenderer.kt b/vector/src/main/java/im/vector/riotredesign/features/media/ImageContentRenderer.kt similarity index 88% rename from vector/src/main/java/im/vector/riotredesign/features/media/MediaContentRenderer.kt rename to vector/src/main/java/im/vector/riotredesign/features/media/ImageContentRenderer.kt index fda674189d..42c50e688b 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/media/MediaContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/media/ImageContentRenderer.kt @@ -27,7 +27,7 @@ import im.vector.riotredesign.core.glide.GlideApp import kotlinx.android.parcel.Parcelize import java.io.File -object MediaContentRenderer { +object ImageContentRenderer { @Parcelize data class Data( @@ -37,8 +37,8 @@ object MediaContentRenderer { val maxHeight: Int, val width: Int?, val maxWidth: Int, - val orientation: Int?, - val rotation: Int? + val orientation: Int? = null, + val rotation: Int? = null ) : Parcelable { fun isLocalFile(): Boolean { @@ -66,6 +66,7 @@ object MediaContentRenderer { GlideApp .with(imageView) .load(resolvedUrl) + .dontAnimate() .thumbnail(0.3f) .into(imageView) } @@ -73,16 +74,12 @@ object MediaContentRenderer { fun render(data: Data, imageView: BigImageView) { val (width, height) = processSize(data, Mode.THUMBNAIL) val contentUrlResolver = Matrix.getInstance().currentSession!!.contentUrlResolver() - if (data.isLocalFile()) { - imageView.showImage(Uri.parse(data.url)) - } else { - val fullSize = contentUrlResolver.resolveFullSize(data.url) - val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE) - imageView.showImage( - Uri.parse(thumbnail), - Uri.parse(fullSize) - ) - } + val fullSize = contentUrlResolver.resolveFullSize(data.url) + val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE) + imageView.showImage( + Uri.parse(thumbnail), + Uri.parse(fullSize) + ) } private fun processSize(data: Data, mode: Mode): Pair { diff --git a/vector/src/main/java/im/vector/riotredesign/features/media/MediaViewerActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/media/ImageMediaViewerActivity.kt similarity index 69% rename from vector/src/main/java/im/vector/riotredesign/features/media/MediaViewerActivity.kt rename to vector/src/main/java/im/vector/riotredesign/features/media/ImageMediaViewerActivity.kt index 1cbd16fe56..902c2f9bc1 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/media/MediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/media/ImageMediaViewerActivity.kt @@ -25,26 +25,26 @@ import androidx.appcompat.widget.Toolbar import com.github.piasy.biv.indicator.progresspie.ProgressPieIndicator import com.github.piasy.biv.view.GlideImageViewFactory import im.vector.riotredesign.core.platform.VectorBaseActivity -import kotlinx.android.synthetic.main.activity_media_viewer.* +import kotlinx.android.synthetic.main.activity_image_media_viewer.* -class MediaViewerActivity : VectorBaseActivity() { +class ImageMediaViewerActivity : VectorBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(im.vector.riotredesign.R.layout.activity_media_viewer) - val mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) + setContentView(im.vector.riotredesign.R.layout.activity_image_media_viewer) + val mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) if (mediaData.url.isNullOrEmpty()) { finish() } else { - configureToolbar(mediaViewerToolbar, mediaData) - mediaViewerImageView.setImageViewFactory(GlideImageViewFactory()) - mediaViewerImageView.setProgressIndicator(ProgressPieIndicator()) - MediaContentRenderer.render(mediaData, mediaViewerImageView) + configureToolbar(imageMediaViewerToolbar, mediaData) + imageMediaViewerImageView.setImageViewFactory(GlideImageViewFactory()) + imageMediaViewerImageView.setProgressIndicator(ProgressPieIndicator()) + ImageContentRenderer.render(mediaData, imageMediaViewerImageView) } } - private fun configureToolbar(toolbar: Toolbar, mediaData: MediaContentRenderer.Data) { + private fun configureToolbar(toolbar: Toolbar, mediaData: ImageContentRenderer.Data) { setSupportActionBar(toolbar) supportActionBar?.apply { title = mediaData.filename @@ -57,8 +57,8 @@ class MediaViewerActivity : VectorBaseActivity() { private const val EXTRA_MEDIA_DATA = "EXTRA_MEDIA_DATA" - fun newIntent(context: Context, mediaData: MediaContentRenderer.Data): Intent { - return Intent(context, MediaViewerActivity::class.java).apply { + fun newIntent(context: Context, mediaData: ImageContentRenderer.Data): Intent { + return Intent(context, ImageMediaViewerActivity::class.java).apply { putExtra(EXTRA_MEDIA_DATA, mediaData) } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/riotredesign/features/media/VideoContentRenderer.kt new file mode 100644 index 0000000000..577b65703d --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/media/VideoContentRenderer.kt @@ -0,0 +1,41 @@ +/* + * 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.riotredesign.features.media + +import android.os.Parcelable +import android.widget.ImageView +import android.widget.VideoView +import im.vector.matrix.android.api.Matrix +import kotlinx.android.parcel.Parcelize + +object VideoContentRenderer { + + @Parcelize + data class Data( + val filename: String, + val videoUrl: String?, + val thumbnailMediaData: ImageContentRenderer.Data + ) : Parcelable + + fun render(data: Data, thumbnailView: ImageView, videoView: VideoView) { + val contentUrlResolver = Matrix.getInstance().currentSession!!.contentUrlResolver() + val resolvedUrl = contentUrlResolver.resolveFullSize(data.videoUrl) + videoView.setVideoPath(resolvedUrl) + videoView.start() + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/media/VideoMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/media/VideoMediaViewerActivity.kt new file mode 100644 index 0000000000..0630a2cb01 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/media/VideoMediaViewerActivity.kt @@ -0,0 +1,62 @@ +/* + * 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.riotredesign.features.media + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.widget.Toolbar +import im.vector.riotredesign.core.platform.VectorBaseActivity +import kotlinx.android.synthetic.main.activity_video_media_viewer.* + + +class VideoMediaViewerActivity : VectorBaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(im.vector.riotredesign.R.layout.activity_video_media_viewer) + val mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) + if (mediaData.videoUrl.isNullOrEmpty()) { + finish() + } else { + configureToolbar(videoMediaViewerToolbar, mediaData) + VideoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerVideoView) + } + } + + private fun configureToolbar(toolbar: Toolbar, mediaData: VideoContentRenderer.Data) { + setSupportActionBar(toolbar) + supportActionBar?.apply { + title = mediaData.filename + setHomeButtonEnabled(true) + setDisplayHomeAsUpEnabled(true) + } + } + + companion object { + + private const val EXTRA_MEDIA_DATA = "EXTRA_MEDIA_DATA" + + fun newIntent(context: Context, mediaData: VideoContentRenderer.Data): Intent { + return Intent(context, VideoMediaViewerActivity::class.java).apply { + putExtra(EXTRA_MEDIA_DATA, mediaData) + } + } + } + + +} \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_media_viewer.xml b/vector/src/main/res/layout/activity_image_media_viewer.xml similarity index 88% rename from vector/src/main/res/layout/activity_media_viewer.xml rename to vector/src/main/res/layout/activity_image_media_viewer.xml index 9927331ed4..3644714bd1 100644 --- a/vector/src/main/res/layout/activity_media_viewer.xml +++ b/vector/src/main/res/layout/activity_image_media_viewer.xml @@ -6,7 +6,7 @@ android:orientation="vertical"> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_file_message.xml b/vector/src/main/res/layout/item_timeline_event_file_message.xml new file mode 100644 index 0000000000..83bc481500 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_file_message.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_image_message.xml b/vector/src/main/res/layout/item_timeline_event_image_video_message.xml similarity index 66% rename from vector/src/main/res/layout/item_timeline_event_image_message.xml rename to vector/src/main/res/layout/item_timeline_event_image_video_message.xml index 21c7b12f9a..b1da588294 100644 --- a/vector/src/main/res/layout/item_timeline_event_image_message.xml +++ b/vector/src/main/res/layout/item_timeline_event_image_video_message.xml @@ -1,4 +1,19 @@ - + + + app:layout_constraintTop_toBottomOf="@+id/messageMemberNameView" + tools:layout_height="300dp" /> + + + diff --git a/vector/src/main/res/layout/item_timeline_event_merged_header.xml b/vector/src/main/res/layout/item_timeline_event_merged_header.xml new file mode 100644 index 0000000000..862a15db6d --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_merged_header.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_text_message.xml b/vector/src/main/res/layout/item_timeline_event_text_message.xml index 5d3004427a..bd6f9f8970 100644 --- a/vector/src/main/res/layout/item_timeline_event_text_message.xml +++ b/vector/src/main/res/layout/item_timeline_event_text_message.xml @@ -44,6 +44,7 @@ android:layout_marginStart="8dp" android:layout_marginLeft="8dp" android:textColor="@color/brown_grey" + android:duplicateParentState="true" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintTop_toTopOf="@id/messageMemberNameView" @@ -56,6 +57,7 @@ android:layout_marginStart="64dp" android:layout_marginLeft="64dp" android:layout_marginBottom="8dp" + android:duplicateParentState="true" android:textColor="@color/dark_grey" android:textSize="14sp" app:layout_constraintBottom_toBottomOf="parent" diff --git a/vector/src/main/res/layout/vector_message_merge_avatar_list.xml b/vector/src/main/res/layout/vector_message_merge_avatar_list.xml new file mode 100644 index 0000000000..d12d82666f --- /dev/null +++ b/vector/src/main/res/layout/vector_message_merge_avatar_list.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + \ No newline at end of file