forked from GitHub-Mirror/riotX-android
Merge pull request #109 from vector-im/feature/timeline_formatting
This commit is contained in:
commit
ceac06caf6
@ -20,7 +20,7 @@ import com.zhuinden.monarchy.Monarchy
|
|||||||
import im.vector.matrix.android.InstrumentedTest
|
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.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.internal.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.DefaultTimeline
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
|
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
||||||
@ -57,7 +57,7 @@ internal class TimelineTest : InstrumentedTest {
|
|||||||
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy)
|
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy)
|
||||||
val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
|
val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
|
||||||
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
|
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
|
||||||
val roomMemberExtractor = RoomMemberExtractor(ROOM_ID)
|
val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID)
|
||||||
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor)
|
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor)
|
||||||
return DefaultTimeline(ROOM_ID, initialEventId, monarchy.realmConfiguration, taskExecutor, getContextOfEventTask, timelineEventFactory, paginationTask, null)
|
return DefaultTimeline(ROOM_ID, initialEventId, monarchy.realmConfiguration, taskExecutor, getContextOfEventTask, timelineEventFactory, paginationTask, null)
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,8 @@ data class ContentAttachmentData(
|
|||||||
val height: Long? = 0,
|
val height: Long? = 0,
|
||||||
val width: Long? = 0,
|
val width: Long? = 0,
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
val path: String? = null,
|
val path: String,
|
||||||
val mimeType: String? = null,
|
val mimeType: String,
|
||||||
val type: Type
|
val type: Type
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
|
@ -18,9 +18,15 @@ package im.vector.matrix.android.api.session.content
|
|||||||
|
|
||||||
interface ContentUploadStateTracker {
|
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 {
|
interface UpdateListener {
|
||||||
fun onUpdate(state: State)
|
fun onUpdate(state: State)
|
||||||
|
@ -29,7 +29,8 @@ data class TimelineEvent(
|
|||||||
val root: Event,
|
val root: Event,
|
||||||
val localId: String,
|
val localId: String,
|
||||||
val displayIndex: Int,
|
val displayIndex: Int,
|
||||||
val roomMember: RoomMember?,
|
val senderName: String?,
|
||||||
|
val senderAvatar: String?,
|
||||||
val sendState: SendState
|
val sendState: SendState
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -56,14 +56,10 @@ internal fun ChunkEntity.merge(roomId: String,
|
|||||||
if (direction == PaginationDirection.FORWARDS) {
|
if (direction == PaginationDirection.FORWARDS) {
|
||||||
this.nextToken = chunkToMerge.nextToken
|
this.nextToken = chunkToMerge.nextToken
|
||||||
this.isLastForward = chunkToMerge.isLastForward
|
this.isLastForward = chunkToMerge.isLastForward
|
||||||
this.forwardsStateIndex = chunkToMerge.forwardsStateIndex
|
|
||||||
this.forwardsDisplayIndex = chunkToMerge.forwardsDisplayIndex
|
|
||||||
eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
|
eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
|
||||||
} else {
|
} else {
|
||||||
this.prevToken = chunkToMerge.prevToken
|
this.prevToken = chunkToMerge.prevToken
|
||||||
this.isLastBackward = chunkToMerge.isLastBackward
|
this.isLastBackward = chunkToMerge.isLastBackward
|
||||||
this.backwardsStateIndex = chunkToMerge.backwardsStateIndex
|
|
||||||
this.backwardsDisplayIndex = chunkToMerge.backwardsDisplayIndex
|
|
||||||
eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
}
|
}
|
||||||
eventsToMerge.forEach {
|
eventsToMerge.forEach {
|
||||||
@ -119,8 +115,8 @@ internal fun ChunkEntity.add(roomId: String,
|
|||||||
this.displayIndex = currentDisplayIndex
|
this.displayIndex = currentDisplayIndex
|
||||||
this.sendState = SendState.SYNCED
|
this.sendState = SendState.SYNCED
|
||||||
}
|
}
|
||||||
// We are not using the order of the list, but will be sorting with displayIndex field
|
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size
|
||||||
events.add(eventEntity)
|
events.add(position, eventEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
|
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
|
||||||
|
@ -31,7 +31,7 @@ internal class ContentModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
ContentUploader(get(), get(), get<ContentUploadStateTracker>() as DefaultContentUploadStateTracker)
|
FileUploader(get(), get())
|
||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
|
@ -23,42 +23,42 @@ import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
|||||||
internal class DefaultContentUploadStateTracker : ContentUploadStateTracker {
|
internal class DefaultContentUploadStateTracker : ContentUploadStateTracker {
|
||||||
|
|
||||||
private val mainHandler = Handler(Looper.getMainLooper())
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
private val progressByEvent = mutableMapOf<String, ContentUploadStateTracker.State>()
|
private val states = mutableMapOf<String, ContentUploadStateTracker.State>()
|
||||||
private val listenersByEvent = mutableMapOf<String, MutableList<ContentUploadStateTracker.UpdateListener>>()
|
private val listeners = mutableMapOf<String, MutableList<ContentUploadStateTracker.UpdateListener>>()
|
||||||
|
|
||||||
override fun track(eventId: String, updateListener: ContentUploadStateTracker.UpdateListener) {
|
override fun track(key: String, updateListener: ContentUploadStateTracker.UpdateListener) {
|
||||||
val listeners = listenersByEvent[eventId] ?: ArrayList()
|
val listeners = listeners[key] ?: ArrayList()
|
||||||
listeners.add(updateListener)
|
listeners.add(updateListener)
|
||||||
listenersByEvent[eventId] = listeners
|
this.listeners[key] = listeners
|
||||||
val currentState = progressByEvent[eventId] ?: ContentUploadStateTracker.State.Idle
|
val currentState = states[key] ?: ContentUploadStateTracker.State.Idle
|
||||||
mainHandler.post { updateListener.onUpdate(currentState) }
|
mainHandler.post { updateListener.onUpdate(currentState) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun untrack(eventId: String, updateListener: ContentUploadStateTracker.UpdateListener) {
|
override fun untrack(key: String, updateListener: ContentUploadStateTracker.UpdateListener) {
|
||||||
listenersByEvent[eventId]?.apply {
|
listeners[key]?.apply {
|
||||||
remove(updateListener)
|
remove(updateListener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun setFailure(eventId: String) {
|
override fun setFailure(key: String) {
|
||||||
val failure = ContentUploadStateTracker.State.Failure
|
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
|
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)
|
val progressData = ContentUploadStateTracker.State.ProgressData(current, total)
|
||||||
updateState(eventId, progressData)
|
updateState(key, progressData)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateState(eventId: String, state: ContentUploadStateTracker.State) {
|
private fun updateState(key: String, state: ContentUploadStateTracker.State) {
|
||||||
progressByEvent[eventId] = state
|
states[key] = state
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
listenersByEvent[eventId]?.also { listeners ->
|
listeners[key]?.also { listeners ->
|
||||||
listeners.forEach { it.onUpdate(state) }
|
listeners.forEach { it.onUpdate(state) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session.content
|
|||||||
import arrow.core.Try
|
import arrow.core.Try
|
||||||
import arrow.core.Try.Companion.raise
|
import arrow.core.Try.Companion.raise
|
||||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
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.di.MoshiProvider
|
||||||
import im.vector.matrix.android.internal.network.ProgressRequestBody
|
import im.vector.matrix.android.internal.network.ProgressRequestBody
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
@ -31,44 +30,51 @@ import java.io.File
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
|
|
||||||
internal class ContentUploader(private val okHttpClient: OkHttpClient,
|
internal class FileUploader(private val okHttpClient: OkHttpClient,
|
||||||
private val sessionParams: SessionParams,
|
private val sessionParams: SessionParams) {
|
||||||
private val contentUploadProgressTracker: DefaultContentUploadStateTracker) {
|
|
||||||
|
private val uploadUrl = sessionParams.homeServerConnectionConfig.homeServerUri.toString() + URI_PREFIX_CONTENT_API + "upload"
|
||||||
|
|
||||||
private val moshi = MoshiProvider.providesMoshi()
|
private val moshi = MoshiProvider.providesMoshi()
|
||||||
private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java)
|
private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java)
|
||||||
|
|
||||||
fun uploadFile(eventId: String, attachment: ContentAttachmentData): Try<ContentUploadResponse> {
|
|
||||||
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()
|
fun uploadFile(file: File,
|
||||||
?: return raise(RuntimeException())
|
filename: String?,
|
||||||
|
mimeType: String,
|
||||||
|
progressListener: ProgressRequestBody.Listener? = null): Try<ContentUploadResponse> {
|
||||||
|
|
||||||
|
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<ContentUploadResponse> {
|
||||||
|
|
||||||
|
val uploadBody = RequestBody.create(MediaType.parse(mimeType), byteArray)
|
||||||
|
return upload(uploadBody, filename, progressListener)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): Try<ContentUploadResponse> {
|
||||||
|
val urlBuilder = HttpUrl.parse(uploadUrl)?.newBuilder() ?: return raise(RuntimeException())
|
||||||
|
|
||||||
val httpUrl = urlBuilder
|
val httpUrl = urlBuilder
|
||||||
.addQueryParameter(
|
.addQueryParameter("filename", filename)
|
||||||
"filename", attachment.name
|
.build()
|
||||||
).build()
|
|
||||||
|
|
||||||
val requestBody = RequestBody.create(
|
val requestBody = if (progressListener != null) ProgressRequestBody(uploadBody, progressListener) else uploadBody
|
||||||
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 request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(httpUrl)
|
.url(httpUrl)
|
||||||
.post(progressRequestBody)
|
.post(requestBody)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val result = Try {
|
return Try {
|
||||||
okHttpClient.newCall(request).execute().use { response ->
|
okHttpClient.newCall(request).execute().use { response ->
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
throw IOException()
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -21,20 +21,29 @@ import androidx.work.CoroutineWorker
|
|||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
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.Event
|
||||||
import im.vector.matrix.android.api.session.events.model.toContent
|
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.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.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.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.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.session.room.send.SendEventWorker
|
||||||
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
||||||
import org.koin.standalone.inject
|
import org.koin.standalone.inject
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
internal class UploadContentWorker(context: Context, params: WorkerParameters)
|
internal class UploadContentWorker(context: Context, params: WorkerParameters)
|
||||||
: CoroutineWorker(context, params), MatrixKoinComponent {
|
: CoroutineWorker(context, params), MatrixKoinComponent {
|
||||||
|
|
||||||
private val mediaUploader by inject<ContentUploader>()
|
private val fileUploader by inject<FileUploader>()
|
||||||
|
private val contentUploadProgressTracker by inject<ContentUploadStateTracker>()
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class Params(
|
internal data class Params(
|
||||||
@ -47,28 +56,65 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters)
|
|||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.failure()
|
?: return Result.failure()
|
||||||
|
|
||||||
if (params.event.eventId == null) {
|
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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 createAttachmentFile(attachment: ContentAttachmentData): File? {
|
||||||
|
return try {
|
||||||
|
File(attachment.path)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleFailure(params: Params): Result {
|
||||||
|
contentUploadProgressTracker.setFailure(params.event.eventId!!)
|
||||||
return Result.failure()
|
return Result.failure()
|
||||||
}
|
}
|
||||||
return mediaUploader
|
|
||||||
.uploadFile(params.event.eventId, params.attachment)
|
|
||||||
.fold({ handleFailure() }, { handleSuccess(params, it) })
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleFailure(): Result {
|
private fun handleSuccess(params: Params,
|
||||||
return Result.retry()
|
attachmentUrl: String,
|
||||||
}
|
thumbnailUrl: String?): Result {
|
||||||
|
contentUploadProgressTracker.setFailure(params.event.eventId!!)
|
||||||
private fun handleSuccess(params: Params, contentUploadResponse: ContentUploadResponse): Result {
|
val event = updateEvent(params.event, attachmentUrl, thumbnailUrl)
|
||||||
val event = updateEvent(params.event, contentUploadResponse.contentUri)
|
|
||||||
val sendParams = SendEventWorker.Params(params.roomId, event)
|
val sendParams = SendEventWorker.Params(params.roomId, event)
|
||||||
return Result.success(WorkerParamsFactory.toData(sendParams))
|
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 messageContent: MessageContent = event.content.toModel() ?: return event
|
||||||
val updatedContent = when (messageContent) {
|
val updatedContent = when (messageContent) {
|
||||||
is MessageImageContent -> messageContent.update(url)
|
is MessageImageContent -> messageContent.update(url)
|
||||||
|
is MessageVideoContent -> messageContent.update(url, thumbnailUrl)
|
||||||
|
is MessageFileContent -> messageContent.update(url)
|
||||||
|
is MessageAudioContent -> messageContent.update(url)
|
||||||
else -> messageContent
|
else -> messageContent
|
||||||
}
|
}
|
||||||
return event.copy(content = updatedContent.toContent())
|
return event.copy(content = updatedContent.toContent())
|
||||||
@ -78,6 +124,18 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters)
|
|||||||
return copy(url = url)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.invite.InviteTask
|
||||||
import im.vector.matrix.android.internal.session.room.members.DefaultRoomMembersService
|
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.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.DefaultReadService
|
||||||
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
|
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
|
||||||
import im.vector.matrix.android.internal.session.room.send.DefaultSendService
|
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) {
|
private val taskExecutor: TaskExecutor) {
|
||||||
|
|
||||||
fun instantiate(roomId: String): Room {
|
fun instantiate(roomId: String): Room {
|
||||||
val roomMemberExtractor = RoomMemberExtractor(roomId)
|
val roomMemberExtractor = SenderRoomMemberExtractor(roomId)
|
||||||
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor)
|
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor)
|
||||||
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask)
|
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask)
|
||||||
val sendService = DefaultSendService(roomId, eventFactory, monarchy)
|
val sendService = DefaultSendService(roomId, eventFactory, monarchy)
|
||||||
|
@ -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<EventEntity> {
|
fun queryRoomMembersEvent(): RealmQuery<EventEntity> {
|
||||||
return EventEntity
|
return EventEntity
|
||||||
.where(realm, roomId, EventType.STATE_ROOM_MEMBER)
|
.where(realm, roomId, EventType.STATE_ROOM_MEMBER)
|
||||||
|
@ -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.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
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.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.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
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.next
|
||||||
import im.vector.matrix.android.internal.database.query.prev
|
import im.vector.matrix.android.internal.database.query.prev
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import io.realm.Realm
|
import io.realm.RealmList
|
||||||
import io.realm.RealmQuery
|
import io.realm.RealmQuery
|
||||||
|
|
||||||
internal class RoomMemberExtractor(private val roomId: String) {
|
internal class SenderRoomMemberExtractor(private val roomId: String) {
|
||||||
|
|
||||||
private val cached = HashMap<String, RoomMember?>()
|
|
||||||
|
|
||||||
fun extractFrom(event: EventEntity): RoomMember? {
|
fun extractFrom(event: EventEntity): RoomMember? {
|
||||||
val sender = event.sender ?: return null
|
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
|
// If the event is unlinked we want to fetch unlinked state events
|
||||||
val unlinked = event.isUnlinked
|
val unlinked = event.isUnlinked
|
||||||
// When stateIndex is negative, we try to get the next stateEvent prevContent()
|
val roomEntity = RoomEntity.where(event.realm, roomId = roomId).findFirst() ?: return null
|
||||||
// If prevContent is null we fallback to the Int.MIN state events content()
|
val chunkEntity = ChunkEntity.findIncludingEvent(event.realm, event.eventId)
|
||||||
val content = if (event.stateIndex <= 0) {
|
val content = when {
|
||||||
baseQuery(event.realm, roomId, sender, unlinked).next(from = event.stateIndex)?.prevContent
|
chunkEntity == null -> null
|
||||||
?: baseQuery(event.realm, roomId, sender, unlinked).prev(since = event.stateIndex)?.content
|
event.stateIndex <= 0 -> baseQuery(chunkEntity.events, sender, unlinked).next(from = event.stateIndex)?.prevContent
|
||||||
} else {
|
else -> baseQuery(chunkEntity.events, sender, unlinked).prev(since = event.stateIndex)?.content
|
||||||
baseQuery(event.realm, roomId, sender, unlinked).prev(since = event.stateIndex)?.content
|
|
||||||
}
|
|
||||||
val roomMember: RoomMember? = ContentMapper.map(content).toModel()
|
|
||||||
cached[cacheKey] = roomMember
|
|
||||||
return roomMember
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun baseQuery(realm: Realm,
|
val fallbackContent = content
|
||||||
roomId: String,
|
?: baseQuery(roomEntity.untimelinedStateEvents, sender, unlinked).prev(since = event.stateIndex)?.content
|
||||||
|
|
||||||
|
return ContentMapper.map(fallbackContent).toModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun baseQuery(list: RealmList<EventEntity>,
|
||||||
sender: String,
|
sender: String,
|
||||||
isUnlinked: Boolean): RealmQuery<EventEntity> {
|
isUnlinked: Boolean): RealmQuery<EventEntity> {
|
||||||
|
return list
|
||||||
val filterMode = if (isUnlinked) EventEntity.LinkFilterMode.UNLINKED_ONLY else EventEntity.LinkFilterMode.LINKED_ONLY
|
.where()
|
||||||
return EventEntity
|
|
||||||
.where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER, linkFilterMode = filterMode)
|
|
||||||
.equalTo(EventEntityFields.STATE_KEY, sender)
|
.equalTo(EventEntityFields.STATE_KEY, sender)
|
||||||
|
.equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER)
|
||||||
|
.equalTo(EventEntityFields.IS_UNLINKED, isUnlinked)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.session.room.send
|
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.auth.data.Credentials
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
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.MessageTextContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
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.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.api.session.room.model.message.VideoInfo
|
||||||
|
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
|
||||||
|
|
||||||
internal class LocalEchoEventFactory(private val credentials: Credentials) {
|
internal class LocalEchoEventFactory(private val credentials: Credentials) {
|
||||||
|
|
||||||
@ -53,7 +56,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) {
|
|||||||
type = MessageType.MSGTYPE_IMAGE,
|
type = MessageType.MSGTYPE_IMAGE,
|
||||||
body = attachment.name ?: "image",
|
body = attachment.name ?: "image",
|
||||||
info = ImageInfo(
|
info = ImageInfo(
|
||||||
mimeType = attachment.mimeType ?: "image/png",
|
mimeType = attachment.mimeType,
|
||||||
width = attachment.width?.toInt() ?: 0,
|
width = attachment.width?.toInt() ?: 0,
|
||||||
height = attachment.height?.toInt() ?: 0,
|
height = attachment.height?.toInt() ?: 0,
|
||||||
size = attachment.size.toInt()
|
size = attachment.size.toInt()
|
||||||
@ -64,15 +67,35 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
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(
|
val content = MessageVideoContent(
|
||||||
type = MessageType.MSGTYPE_VIDEO,
|
type = MessageType.MSGTYPE_VIDEO,
|
||||||
body = attachment.name ?: "video",
|
body = attachment.name ?: "video",
|
||||||
info = VideoInfo(
|
info = VideoInfo(
|
||||||
mimeType = attachment.mimeType ?: "video/mpeg",
|
mimeType = attachment.mimeType,
|
||||||
width = attachment.width?.toInt() ?: 0,
|
width = width,
|
||||||
height = attachment.height?.toInt() ?: 0,
|
height = height,
|
||||||
size = attachment.size,
|
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
|
url = attachment.path
|
||||||
)
|
)
|
||||||
|
@ -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.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.util.CancelableBag
|
import im.vector.matrix.android.api.util.CancelableBag
|
||||||
import im.vector.matrix.android.api.util.addTo
|
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.findIncludingEvent
|
||||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
import im.vector.matrix.android.internal.util.Debouncer
|
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 timber.log.Timber
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
@ -90,6 +100,13 @@ internal class DefaultTimeline(
|
|||||||
if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL ) {
|
if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL ) {
|
||||||
handleInitialLoad()
|
handleInitialLoad()
|
||||||
} else {
|
} 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 ->
|
changeSet.insertionRanges.forEach { range ->
|
||||||
val (startDisplayIndex, direction) = if (range.startIndex == 0) {
|
val (startDisplayIndex, direction) = if (range.startIndex == 0) {
|
||||||
Pair(liveEvents[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS)
|
Pair(liveEvents[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS)
|
||||||
@ -108,6 +125,7 @@ internal class DefaultTimeline(
|
|||||||
buildTimelineEvents(startDisplayIndex, direction, range.length.toLong())
|
buildTimelineEvents(startDisplayIndex, direction, range.length.toLong())
|
||||||
postSnapshot()
|
postSnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
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<String, SenderData>()
|
||||||
|
|
||||||
fun create(eventEntity: EventEntity): TimelineEvent {
|
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(
|
return TimelineEvent(
|
||||||
eventEntity.asDomain(),
|
eventEntity.asDomain(),
|
||||||
eventEntity.localId,
|
eventEntity.localId,
|
||||||
eventEntity.displayIndex,
|
eventEntity.displayIndex,
|
||||||
roomMember,
|
senderData.senderName,
|
||||||
|
senderData.senderAvatar,
|
||||||
eventEntity.sendState
|
eventEntity.sendState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clear(){
|
||||||
|
cached.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class SenderData(
|
||||||
|
val senderName: String?,
|
||||||
|
val senderAvatar: String?
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
@ -187,6 +187,7 @@ dependencies {
|
|||||||
implementation "com.github.piasy:GlideImageViewFactory:$big_image_viewer_version"
|
implementation "com.github.piasy:GlideImageViewFactory:$big_image_viewer_version"
|
||||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||||
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||||
|
implementation 'com.danikula:videocache:2.7.1'
|
||||||
|
|
||||||
// Badge for compatibility
|
// Badge for compatibility
|
||||||
implementation 'me.leolin:ShortcutBadger:1.1.2@aar'
|
implementation 'me.leolin:ShortcutBadger:1.1.2@aar'
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
<activity android:name=".features.home.HomeActivity" />
|
<activity android:name=".features.home.HomeActivity" />
|
||||||
<activity android:name=".features.login.LoginActivity" />
|
<activity android:name=".features.login.LoginActivity" />
|
||||||
<activity android:name=".features.media.MediaViewerActivity" />
|
<activity android:name=".features.media.ImageMediaViewerActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".features.rageshake.BugReportActivity"
|
android:name=".features.rageshake.BugReportActivity"
|
||||||
android:label="@string/title_activity_bug_report" />
|
android:label="@string/title_activity_bug_report" />
|
||||||
@ -37,6 +37,7 @@
|
|||||||
android:name=".features.settings.VectorSettingsActivity"
|
android:name=".features.settings.VectorSettingsActivity"
|
||||||
android:label="@string/title_activity_settings"
|
android:label="@string/title_activity_settings"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
<activity android:name=".features.media.VideoMediaViewerActivity" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".core.services.CallService"
|
android:name=".core.services.CallService"
|
||||||
|
@ -32,6 +32,7 @@ import com.bumptech.glide.util.Util
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import im.vector.riotredesign.BuildConfig
|
import im.vector.riotredesign.BuildConfig
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.utils.toast
|
||||||
import im.vector.riotredesign.features.rageshake.BugReportActivity
|
import im.vector.riotredesign.features.rageshake.BugReportActivity
|
||||||
import im.vector.riotredesign.features.rageshake.BugReporter
|
import im.vector.riotredesign.features.rageshake.BugReporter
|
||||||
import im.vector.riotredesign.features.rageshake.RageShake
|
import im.vector.riotredesign.features.rageshake.RageShake
|
||||||
@ -284,9 +285,9 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
|
|||||||
* PUBLIC METHODS
|
* PUBLIC METHODS
|
||||||
* ========================================================================================== */
|
* ========================================================================================== */
|
||||||
|
|
||||||
protected fun showSnackbar(message: String) {
|
fun showSnackbar(message: String) {
|
||||||
coordinatorLayout?.let {
|
coordinatorLayout?.let {
|
||||||
Snackbar.make(it, message, Snackbar.LENGTH_SHORT)
|
Snackbar.make(it, message, Snackbar.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,8 +295,8 @@ abstract class VectorBaseActivity : BaseMvRxActivity() {
|
|||||||
* Temporary method
|
* Temporary method
|
||||||
* ========================================================================================== */
|
* ========================================================================================== */
|
||||||
|
|
||||||
protected fun notImplemented() {
|
fun notImplemented() {
|
||||||
showSnackbar(getString(R.string.not_implemented))
|
toast(getString(R.string.not_implemented))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -31,6 +31,7 @@ class AutocompleteUserController : TypedEpoxyController<List<User>>() {
|
|||||||
data.forEach { user ->
|
data.forEach { user ->
|
||||||
autocompleteUserItem {
|
autocompleteUserItem {
|
||||||
id(user.userId)
|
id(user.userId)
|
||||||
|
userId(user.userId)
|
||||||
name(user.displayName)
|
name(user.displayName)
|
||||||
avatarUrl(user.avatarUrl)
|
avatarUrl(user.avatarUrl)
|
||||||
clickListener { _ ->
|
clickListener { _ ->
|
||||||
|
@ -29,18 +29,15 @@ import im.vector.riotredesign.features.home.AvatarRenderer
|
|||||||
@EpoxyModelClass(layout = R.layout.item_autocomplete_user)
|
@EpoxyModelClass(layout = R.layout.item_autocomplete_user)
|
||||||
abstract class AutocompleteUserItem : VectorEpoxyModel<AutocompleteUserItem.Holder>() {
|
abstract class AutocompleteUserItem : VectorEpoxyModel<AutocompleteUserItem.Holder>() {
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute var name: String? = null
|
||||||
var name: String? = null
|
@EpoxyAttribute var userId: String = ""
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute var avatarUrl: String? = null
|
||||||
var avatarUrl: String? = null
|
@EpoxyAttribute var clickListener: View.OnClickListener? = null
|
||||||
@EpoxyAttribute
|
|
||||||
var clickListener: View.OnClickListener? = null
|
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
holder.view.setOnClickListener(clickListener)
|
holder.view.setOnClickListener(clickListener)
|
||||||
|
|
||||||
holder.nameView.text = name
|
holder.nameView.text = name
|
||||||
AvatarRenderer.render(avatarUrl, name, holder.avatarImageView)
|
AvatarRenderer.render(avatarUrl, userId, name, holder.avatarImageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Holder : VectorEpoxyHolder() {
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
@ -29,7 +29,6 @@ import com.bumptech.glide.request.target.Target
|
|||||||
import im.vector.matrix.android.api.Matrix
|
import im.vector.matrix.android.api.Matrix
|
||||||
import im.vector.matrix.android.api.MatrixPatterns
|
import im.vector.matrix.android.api.MatrixPatterns
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
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.matrix.android.api.session.room.model.RoomSummary
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.glide.GlideApp
|
import im.vector.riotredesign.core.glide.GlideApp
|
||||||
@ -44,39 +43,41 @@ object AvatarRenderer {
|
|||||||
|
|
||||||
private const val THUMBNAIL_SIZE = 250
|
private const val THUMBNAIL_SIZE = 250
|
||||||
|
|
||||||
@UiThread
|
private val AVATAR_COLOR_LIST = listOf(
|
||||||
fun render(roomMember: RoomMember, imageView: ImageView) {
|
R.color.avatar_color_1,
|
||||||
render(roomMember.avatarUrl, roomMember.displayName, imageView)
|
R.color.avatar_color_2,
|
||||||
}
|
R.color.avatar_color_3
|
||||||
|
)
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
fun render(roomSummary: RoomSummary, imageView: ImageView) {
|
fun render(roomSummary: RoomSummary, imageView: ImageView) {
|
||||||
render(roomSummary.avatarUrl, roomSummary.displayName, imageView)
|
render(roomSummary.avatarUrl, roomSummary.roomId, roomSummary.displayName, imageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
fun render(avatarUrl: String?, name: String?, imageView: ImageView) {
|
fun render(avatarUrl: String?, identifier: String, name: String?, imageView: ImageView) {
|
||||||
render(imageView.context, GlideApp.with(imageView), avatarUrl, name, DrawableImageViewTarget(imageView))
|
render(imageView.context, GlideApp.with(imageView), avatarUrl, identifier, name, DrawableImageViewTarget(imageView))
|
||||||
}
|
}
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
fun render(context: Context,
|
fun render(context: Context,
|
||||||
glideRequest: GlideRequests,
|
glideRequest: GlideRequests,
|
||||||
avatarUrl: String?,
|
avatarUrl: String?,
|
||||||
|
identifier: String,
|
||||||
name: String?,
|
name: String?,
|
||||||
target: Target<Drawable>) {
|
target: Target<Drawable>) {
|
||||||
if (name.isNullOrEmpty()) {
|
if (name.isNullOrEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val placeholder = getPlaceholderDrawable(context, name)
|
val placeholder = getPlaceholderDrawable(context, identifier, name)
|
||||||
buildGlideRequest(glideRequest, avatarUrl)
|
buildGlideRequest(glideRequest, avatarUrl)
|
||||||
.placeholder(placeholder)
|
.placeholder(placeholder)
|
||||||
.into(target)
|
.into(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
@AnyThread
|
@AnyThread
|
||||||
fun getPlaceholderDrawable(context: Context, text: String): Drawable {
|
fun getPlaceholderDrawable(context: Context, identifier: String, text: String): Drawable {
|
||||||
val avatarColor = ContextCompat.getColor(context, R.color.pale_teal)
|
val avatarColor = ContextCompat.getColor(context, getAvatarColor(identifier))
|
||||||
return if (text.isEmpty()) {
|
return if (text.isEmpty()) {
|
||||||
TextDrawable.builder().buildRound("", avatarColor)
|
TextDrawable.builder().buildRound("", avatarColor)
|
||||||
} else {
|
} else {
|
||||||
@ -87,9 +88,21 @@ object AvatarRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// PRIVATE API *********************************************************************************
|
// 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<Drawable> {
|
private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> {
|
||||||
val resolvedUrl = Matrix.getInstance().currentSession!!.contentUrlResolver()
|
val resolvedUrl = Matrix.getInstance().currentSession!!.contentUrlResolver()
|
||||||
.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE)
|
.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||||
|
@ -35,6 +35,7 @@ class GroupSummaryController : TypedEpoxyController<GroupListViewState>() {
|
|||||||
val isSelected = groupSummary.groupId == selected?.groupId
|
val isSelected = groupSummary.groupId == selected?.groupId
|
||||||
groupSummaryItem {
|
groupSummaryItem {
|
||||||
id(groupSummary.groupId)
|
id(groupSummary.groupId)
|
||||||
|
groupId(groupSummary.groupId)
|
||||||
groupName(groupSummary.displayName)
|
groupName(groupSummary.displayName)
|
||||||
selected(isSelected)
|
selected(isSelected)
|
||||||
avatarUrl(groupSummary.avatarUrl)
|
avatarUrl(groupSummary.avatarUrl)
|
||||||
|
@ -29,6 +29,7 @@ import im.vector.riotredesign.features.home.AvatarRenderer
|
|||||||
abstract class GroupSummaryItem : VectorEpoxyModel<GroupSummaryItem.Holder>() {
|
abstract class GroupSummaryItem : VectorEpoxyModel<GroupSummaryItem.Holder>() {
|
||||||
|
|
||||||
@EpoxyAttribute lateinit var groupName: CharSequence
|
@EpoxyAttribute lateinit var groupName: CharSequence
|
||||||
|
@EpoxyAttribute lateinit var groupId: String
|
||||||
@EpoxyAttribute var avatarUrl: String? = null
|
@EpoxyAttribute var avatarUrl: String? = null
|
||||||
@EpoxyAttribute var selected: Boolean = false
|
@EpoxyAttribute var selected: Boolean = false
|
||||||
@EpoxyAttribute var listener: (() -> Unit)? = null
|
@EpoxyAttribute var listener: (() -> Unit)? = null
|
||||||
@ -37,7 +38,7 @@ abstract class GroupSummaryItem : VectorEpoxyModel<GroupSummaryItem.Holder>() {
|
|||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
holder.rootView.isSelected = selected
|
holder.rootView.isSelected = selected
|
||||||
holder.rootView.setOnClickListener { listener?.invoke() }
|
holder.rootView.setOnClickListener { listener?.invoke() }
|
||||||
AvatarRenderer.render(avatarUrl, groupName.toString(), holder.avatarImageView)
|
AvatarRenderer.render(avatarUrl, groupId, groupName.toString(), holder.avatarImageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Holder : VectorEpoxyHolder() {
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
@ -37,6 +37,10 @@ import com.otaliastudios.autocomplete.Autocomplete
|
|||||||
import com.otaliastudios.autocomplete.AutocompleteCallback
|
import com.otaliastudios.autocomplete.AutocompleteCallback
|
||||||
import com.otaliastudios.autocomplete.CharPolicy
|
import com.otaliastudios.autocomplete.CharPolicy
|
||||||
import im.vector.matrix.android.api.session.Session
|
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.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.user.model.User
|
import im.vector.matrix.android.api.session.user.model.User
|
||||||
import im.vector.riotredesign.R
|
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.TimelineEventController
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
|
import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
|
||||||
import im.vector.riotredesign.features.html.PillImageSpan
|
import im.vector.riotredesign.features.html.PillImageSpan
|
||||||
import im.vector.riotredesign.features.media.MediaContentRenderer
|
import im.vector.riotredesign.features.media.ImageContentRenderer
|
||||||
import im.vector.riotredesign.features.media.MediaViewerActivity
|
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.parcel.Parcelize
|
||||||
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
@ -377,11 +383,24 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
|||||||
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event))
|
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View) {
|
override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) {
|
||||||
val intent = MediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
|
val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// AutocompleteUserPresenter.Callback
|
||||||
|
|
||||||
override fun onQueryUsers(query: CharSequence?) {
|
override fun onQueryUsers(query: CharSequence?) {
|
||||||
|
@ -24,16 +24,32 @@ import androidx.recyclerview.widget.ListUpdateCallback
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.airbnb.epoxy.EpoxyController
|
import com.airbnb.epoxy.EpoxyController
|
||||||
import com.airbnb.epoxy.EpoxyModel
|
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.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.riotredesign.core.epoxy.LoadingItemModel_
|
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.core.extensions.localDateTime
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory
|
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.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,
|
class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||||
private val timelineItemFactory: TimelineItemFactory,
|
private val timelineItemFactory: TimelineItemFactory,
|
||||||
@ -44,10 +60,16 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
interface Callback {
|
interface Callback {
|
||||||
fun onEventVisible(event: TimelineEvent)
|
fun onEventVisible(event: TimelineEvent)
|
||||||
fun onUrlClicked(url: String)
|
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<List<EpoxyModel<*>>>()
|
private val collapsedEventIds = linkedSetOf<String>()
|
||||||
|
private val mergeItemCollapseStates = HashMap<String, Boolean>()
|
||||||
|
private val modelCache = arrayListOf<CacheItemData?>()
|
||||||
|
|
||||||
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
||||||
private var inSubmitList: Boolean = false
|
private var inSubmitList: Boolean = false
|
||||||
private var timeline: Timeline? = null
|
private var timeline: Timeline? = null
|
||||||
@ -60,7 +82,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||||
assertUpdateCallbacksAllowed()
|
assertUpdateCallbacksAllowed()
|
||||||
(position until (position + count)).forEach {
|
(position until (position + count)).forEach {
|
||||||
modelCache[it] = emptyList()
|
modelCache[it] = null
|
||||||
}
|
}
|
||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
}
|
}
|
||||||
@ -76,11 +98,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
override fun onInserted(position: Int, count: Int) {
|
override fun onInserted(position: Int, count: Int) {
|
||||||
assertUpdateCallbacksAllowed()
|
assertUpdateCallbacksAllowed()
|
||||||
if (modelCache.isNotEmpty() && position == modelCache.size) {
|
|
||||||
modelCache[position - 1] = emptyList()
|
|
||||||
}
|
|
||||||
(0 until count).forEach {
|
(0 until count).forEach {
|
||||||
modelCache.add(position, emptyList())
|
modelCache.add(position, null)
|
||||||
}
|
}
|
||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
}
|
}
|
||||||
@ -116,7 +135,6 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
.id("forward_loading_item")
|
.id("forward_loading_item")
|
||||||
.addWhen(Timeline.Direction.FORWARDS)
|
.addWhen(Timeline.Direction.FORWARDS)
|
||||||
|
|
||||||
|
|
||||||
val timelineModels = getModels()
|
val timelineModels = getModels()
|
||||||
add(timelineModels)
|
add(timelineModels)
|
||||||
|
|
||||||
@ -149,53 +167,110 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
private fun getModels(): List<EpoxyModel<*>> {
|
private fun getModels(): List<EpoxyModel<*>> {
|
||||||
(0 until modelCache.size).forEach { position ->
|
(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)
|
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<TimelineEvent>): List<EpoxyModel<*>> {
|
|
||||||
val epoxyModels = ArrayList<EpoxyModel<*>>()
|
private fun buildItemModels(currentPosition: Int, items: List<TimelineEvent>): CacheItemData {
|
||||||
val event = items[currentPosition]
|
val event = items[currentPosition]
|
||||||
val nextEvent = items.nextDisplayableEvent(currentPosition)
|
val nextEvent = items.nextDisplayableEvent(currentPosition)
|
||||||
|
|
||||||
val date = event.root.localDateTime()
|
val date = event.root.localDateTime()
|
||||||
val nextDate = nextEvent?.root?.localDateTime()
|
val nextDate = nextEvent?.root?.localDateTime()
|
||||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
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.id(event.localId)
|
||||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
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 formattedDay = dateFormatter.formatMessageDay(date)
|
||||||
val daySeparatorItem = DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
|
DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
|
||||||
epoxyModels.add(daySeparatorItem)
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildMergedHeaderItem(event: TimelineEvent,
|
||||||
|
nextEvent: TimelineEvent?,
|
||||||
|
items: List<TimelineEvent>,
|
||||||
|
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) {
|
private fun LoadingItemModel_.addWhen(direction: Timeline.Direction) {
|
||||||
val shouldAdd = timeline?.let {
|
val shouldAdd = timeline?.hasMoreToLoad(direction) ?: false
|
||||||
it.hasMoreToLoad(direction)
|
|
||||||
} ?: false
|
|
||||||
addIf(shouldAdd, this@TimelineEventController)
|
addIf(shouldAdd, this@TimelineEventController)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?,
|
private data class CacheItemData(
|
||||||
private val event: TimelineEvent)
|
val localId: String,
|
||||||
: VectorEpoxyModel.OnVisibilityStateChangedListener {
|
val eventModel: EpoxyModel<*>? = null,
|
||||||
|
val mergedHeaderModel: MergedHeaderItem? = null,
|
||||||
override fun onVisibilityStateChanged(visibilityState: Int) {
|
val formattedDayModel: DaySeparatorItem? = null
|
||||||
if (visibilityState == VisibilityState.VISIBLE) {
|
)
|
||||||
callback?.onEventVisible(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -30,27 +30,26 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
|
|||||||
class CallItemFactory(private val stringProvider: StringProvider) {
|
class CallItemFactory(private val stringProvider: StringProvider) {
|
||||||
|
|
||||||
fun create(event: TimelineEvent): NoticeItem? {
|
fun create(event: TimelineEvent): NoticeItem? {
|
||||||
val roomMember = event.roomMember ?: return null
|
val text = buildNoticeText(event.root, event.senderName) ?: return null
|
||||||
val text = buildNoticeText(event.root, roomMember) ?: return null
|
|
||||||
return NoticeItem_()
|
return NoticeItem_()
|
||||||
.noticeText(text)
|
.noticeText(text)
|
||||||
.avatarUrl(roomMember.avatarUrl)
|
.avatarUrl(event.senderAvatar)
|
||||||
.memberName(roomMember.displayName)
|
.memberName(event.senderName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildNoticeText(event: Event, roomMember: RoomMember): CharSequence? {
|
private fun buildNoticeText(event: Event, senderName: String?): CharSequence? {
|
||||||
return when {
|
return when {
|
||||||
EventType.CALL_INVITE == event.type -> {
|
EventType.CALL_INVITE == event.type -> {
|
||||||
val content = event.content.toModel<CallInviteContent>() ?: return null
|
val content = event.content.toModel<CallInviteContent>() ?: return null
|
||||||
val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO
|
val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO
|
||||||
return if (isVideoCall) {
|
return if (isVideoCall) {
|
||||||
stringProvider.getString(R.string.notice_placed_video_call, roomMember.displayName)
|
stringProvider.getString(R.string.notice_placed_video_call, senderName)
|
||||||
} else {
|
} 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_ANSWER == event.type -> stringProvider.getString(R.string.notice_answered_call, senderName)
|
||||||
EventType.CALL_HANGUP == event.type -> stringProvider.getString(R.string.notice_ended_call, roomMember.displayName)
|
EventType.CALL_HANGUP == event.type -> stringProvider.getString(R.string.notice_ended_call, senderName)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,16 +18,19 @@ package im.vector.riotredesign.features.home.room.detail.timeline.factory
|
|||||||
|
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
||||||
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
|
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.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
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.MessageContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent
|
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.MessageImageContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent
|
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.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.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
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.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.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.MessageFileItem
|
||||||
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.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.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.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.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
|
import me.gujun.android.span.span
|
||||||
|
|
||||||
class MessageItemFactory(private val colorProvider: ColorProvider,
|
class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||||
@ -59,8 +65,6 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
): VectorEpoxyModel<*>? {
|
): VectorEpoxyModel<*>? {
|
||||||
|
|
||||||
val eventId = event.root.eventId ?: return null
|
val eventId = event.root.eventId ?: return null
|
||||||
val roomMember = event.roomMember
|
|
||||||
val nextRoomMember = nextEvent?.roomMember
|
|
||||||
|
|
||||||
val date = event.root.localDateTime()
|
val date = event.root.localDateTime()
|
||||||
val nextDate = nextEvent?.root?.localDateTime()
|
val nextDate = nextEvent?.root?.localDateTime()
|
||||||
@ -69,56 +73,113 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
?: false
|
?: false
|
||||||
|
|
||||||
val showInformation = addDaySeparator
|
val showInformation = addDaySeparator
|
||||||
|| nextRoomMember != roomMember
|
|| event.senderAvatar != nextEvent?.senderAvatar
|
||||||
|
|| event.senderName != nextEvent?.senderName
|
||||||
|| nextEvent?.root?.type != EventType.MESSAGE
|
|| nextEvent?.root?.type != EventType.MESSAGE
|
||||||
|| isNextMessageReceivedMoreThanOneHourAgo
|
|| isNextMessageReceivedMoreThanOneHourAgo
|
||||||
|
|
||||||
val messageContent: MessageContent = event.root.content.toModel() ?: return null
|
val messageContent: MessageContent = event.root.content.toModel() ?: return null
|
||||||
val time = timelineDateFormatter.formatMessageHour(date)
|
val time = timelineDateFormatter.formatMessageHour(date)
|
||||||
val avatarUrl = roomMember?.avatarUrl
|
val avatarUrl = event.senderAvatar
|
||||||
val memberName = roomMember?.displayName ?: event.root.sender
|
val memberName = event.senderName ?: event.root.sender ?: ""
|
||||||
val informationData = MessageInformationData(time, avatarUrl, memberName, showInformation)
|
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) {
|
return when (messageContent) {
|
||||||
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback)
|
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback)
|
||||||
is MessageTextContent -> buildTextMessageItem(event.sendState, messageContent, informationData, callback)
|
is MessageTextContent -> buildTextMessageItem(messageContent, informationData, callback)
|
||||||
is MessageImageContent -> buildImageMessageItem(eventId, messageContent, informationData, callback)
|
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback)
|
||||||
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback)
|
|
||||||
is MessageNoticeContent -> buildNoticeMessageItem(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)
|
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? {
|
private fun buildNotHandledMessageItem(messageContent: MessageContent): DefaultItem? {
|
||||||
val text = "${messageContent.type} message events are not yet handled"
|
val text = "${messageContent.type} message events are not yet handled"
|
||||||
return DefaultItem_().text(text)
|
return DefaultItem_().text(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildImageMessageItem(eventId: String,
|
private fun buildImageMessageItem(messageContent: MessageImageContent,
|
||||||
messageContent: MessageImageContent,
|
|
||||||
informationData: MessageInformationData,
|
informationData: MessageInformationData,
|
||||||
callback: TimelineEventController.Callback?): MessageImageItem? {
|
callback: TimelineEventController.Callback?): MessageImageVideoItem? {
|
||||||
|
|
||||||
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
||||||
val data = MediaContentRenderer.Data(
|
val data = ImageContentRenderer.Data(
|
||||||
messageContent.body,
|
filename = messageContent.body,
|
||||||
url = messageContent.url,
|
url = messageContent.url,
|
||||||
height = messageContent.info?.height,
|
height = messageContent.info?.height,
|
||||||
maxHeight = maxHeight,
|
maxHeight = maxHeight,
|
||||||
width = messageContent.info?.width,
|
width = messageContent.info?.width,
|
||||||
maxWidth = maxWidth,
|
maxWidth = maxWidth,
|
||||||
rotation = messageContent.info?.rotation,
|
orientation = messageContent.info?.orientation,
|
||||||
orientation = messageContent.info?.orientation
|
rotation = messageContent.info?.rotation
|
||||||
)
|
)
|
||||||
return MessageImageItem_()
|
return MessageImageVideoItem_()
|
||||||
.eventId(eventId)
|
.playable(messageContent.info?.mimeType == "image/gif")
|
||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
.mediaData(data)
|
.mediaData(data)
|
||||||
.clickListener { view -> callback?.onMediaClicked(data, view) }
|
.clickListener { view -> callback?.onImageMessageClicked(messageContent, data, view) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildTextMessageItem(sendState: SendState,
|
private fun buildVideoMessageItem(messageContent: MessageVideoContent,
|
||||||
messageContent: MessageTextContent,
|
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,
|
informationData: MessageInformationData,
|
||||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||||
|
|
||||||
@ -126,15 +187,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
htmlRenderer.render(it)
|
htmlRenderer.render(it)
|
||||||
} ?: messageContent.body
|
} ?: messageContent.body
|
||||||
|
|
||||||
val textColor = if (sendState.isSent()) {
|
val linkifiedBody = linkifyBody(bodyToUse, callback)
|
||||||
R.color.dark_grey
|
|
||||||
} else {
|
|
||||||
R.color.brown_grey
|
|
||||||
}
|
|
||||||
val formattedBody = span(bodyToUse) {
|
|
||||||
this.textColor = colorProvider.getColor(textColor)
|
|
||||||
}
|
|
||||||
val linkifiedBody = linkifyBody(formattedBody, callback)
|
|
||||||
return MessageTextItem_()
|
return MessageTextItem_()
|
||||||
.message(linkifiedBody)
|
.message(linkifiedBody)
|
||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
@ -181,4 +234,31 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
return spannable
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -31,15 +31,14 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem
|
|||||||
class RoomHistoryVisibilityItemFactory(private val stringProvider: StringProvider) {
|
class RoomHistoryVisibilityItemFactory(private val stringProvider: StringProvider) {
|
||||||
|
|
||||||
fun create(event: TimelineEvent): NoticeItem? {
|
fun create(event: TimelineEvent): NoticeItem? {
|
||||||
val roomMember = event.roomMember ?: return null
|
val noticeText = buildNoticeText(event.root, event.senderName) ?: return null
|
||||||
val noticeText = buildNoticeText(event.root, roomMember) ?: return null
|
|
||||||
return NoticeItem_()
|
return NoticeItem_()
|
||||||
.noticeText(noticeText)
|
.noticeText(noticeText)
|
||||||
.avatarUrl(roomMember.avatarUrl)
|
.avatarUrl(event.senderAvatar)
|
||||||
.memberName(roomMember.displayName)
|
.memberName(event.senderName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildNoticeText(event: Event, roomMember: RoomMember): CharSequence? {
|
private fun buildNoticeText(event: Event, senderName: String?): CharSequence? {
|
||||||
val content = event.content.toModel<RoomHistoryVisibilityContent>() ?: return null
|
val content = event.content.toModel<RoomHistoryVisibilityContent>() ?: return null
|
||||||
val formattedVisibility = when (content.historyVisibility) {
|
val formattedVisibility = when (content.historyVisibility) {
|
||||||
RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared)
|
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.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined)
|
||||||
RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.resources.StringProvider
|
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
|
||||||
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) {
|
class RoomMemberItemFactory(private val stringProvider: StringProvider) {
|
||||||
|
|
||||||
fun create(event: TimelineEvent): NoticeItem? {
|
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 eventContent: RoomMember? = event.root.content.toModel()
|
||||||
val prevEventContent: RoomMember? = event.root.prevContent.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
|
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
|
||||||
return if (isMembershipEvent) {
|
return if (isMembershipEvent) {
|
||||||
buildMembershipNotice(event, eventContent, prevEventContent)
|
buildMembershipNotice(event, eventContent, prevEventContent)
|
||||||
@ -71,7 +75,7 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) {
|
|||||||
displayText.append(" ")
|
displayText.append(" ")
|
||||||
stringProvider.getString(R.string.notice_avatar_changed_too)
|
stringProvider.getString(R.string.notice_avatar_changed_too)
|
||||||
} else {
|
} 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)
|
displayText.append(displayAvatarText)
|
||||||
}
|
}
|
||||||
@ -79,12 +83,12 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildMembershipNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
|
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
|
val targetDisplayName = eventContent?.displayName ?: event.root.sender
|
||||||
return when {
|
return when {
|
||||||
Membership.INVITE == eventContent?.membership -> {
|
Membership.INVITE == eventContent?.membership -> {
|
||||||
// TODO get userId
|
// TODO get userId
|
||||||
val selfUserId: String = ""
|
val selfUserId = ""
|
||||||
when {
|
when {
|
||||||
eventContent.thirdPartyInvite != null ->
|
eventContent.thirdPartyInvite != null ->
|
||||||
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
|
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
|
||||||
@ -105,7 +109,8 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) {
|
|||||||
if (prevEventContent?.membership == Membership.INVITE) {
|
if (prevEventContent?.membership == Membership.INVITE) {
|
||||||
stringProvider.getString(R.string.notice_room_reject, senderDisplayName)
|
stringProvider.getString(R.string.notice_room_reject, senderDisplayName)
|
||||||
} else {
|
} 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) {
|
} else if (prevEventContent?.membership == Membership.INVITE) {
|
||||||
stringProvider.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName)
|
stringProvider.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName)
|
||||||
|
@ -29,20 +29,16 @@ class RoomNameItemFactory(private val stringProvider: StringProvider) {
|
|||||||
|
|
||||||
fun create(event: TimelineEvent): NoticeItem? {
|
fun create(event: TimelineEvent): NoticeItem? {
|
||||||
|
|
||||||
val content: RoomNameContent? = event.root.content.toModel()
|
val content: RoomNameContent = event.root.content.toModel() ?: return null
|
||||||
val roomMember = event.roomMember
|
|
||||||
if (content == null || roomMember == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val text = if (!TextUtils.isEmpty(content.name)) {
|
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 {
|
} else {
|
||||||
stringProvider.getString(R.string.notice_room_name_removed, roomMember.displayName)
|
stringProvider.getString(R.string.notice_room_name_removed, event.senderName)
|
||||||
}
|
}
|
||||||
return NoticeItem_()
|
return NoticeItem_()
|
||||||
.noticeText(text)
|
.noticeText(text)
|
||||||
.avatarUrl(roomMember.avatarUrl)
|
.avatarUrl(event.senderAvatar)
|
||||||
.memberName(roomMember.displayName)
|
.memberName(event.senderName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,20 +28,16 @@ class RoomTopicItemFactory(private val stringProvider: StringProvider) {
|
|||||||
|
|
||||||
fun create(event: TimelineEvent): NoticeItem? {
|
fun create(event: TimelineEvent): NoticeItem? {
|
||||||
|
|
||||||
val content: RoomTopicContent? = event.root.content.toModel()
|
val content: RoomTopicContent = event.root.content.toModel() ?: return null
|
||||||
val roomMember = event.roomMember
|
|
||||||
if (content == null || roomMember == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val text = if (content.topic.isNullOrEmpty()) {
|
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 {
|
} 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_()
|
return NoticeItem_()
|
||||||
.noticeText(text)
|
.noticeText(text)
|
||||||
.avatarUrl(roomMember.avatarUrl)
|
.avatarUrl(event.senderAvatar)
|
||||||
.memberName(roomMember.displayName)
|
.memberName(event.senderName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
defaultItemFactory.create(event, e)
|
defaultItemFactory.create(event, e)
|
||||||
}
|
}
|
||||||
return computedModel ?: EmptyItem_()
|
return (computedModel ?: EmptyItem_())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -25,7 +25,7 @@ import android.widget.TextView
|
|||||||
import im.vector.matrix.android.api.Matrix
|
import im.vector.matrix.android.api.Matrix
|
||||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.features.media.MediaContentRenderer
|
import im.vector.riotredesign.features.media.ImageContentRenderer
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object ContentUploadStateTrackerBinder {
|
object ContentUploadStateTrackerBinder {
|
||||||
@ -33,7 +33,7 @@ object ContentUploadStateTrackerBinder {
|
|||||||
private val updateListeners = mutableMapOf<String, ContentUploadStateTracker.UpdateListener>()
|
private val updateListeners = mutableMapOf<String, ContentUploadStateTracker.UpdateListener>()
|
||||||
|
|
||||||
fun bind(eventId: String,
|
fun bind(eventId: String,
|
||||||
mediaData: MediaContentRenderer.Data,
|
mediaData: ImageContentRenderer.Data,
|
||||||
progressLayout: ViewGroup) {
|
progressLayout: ViewGroup) {
|
||||||
|
|
||||||
Matrix.getInstance().currentSession?.also { session ->
|
Matrix.getInstance().currentSession?.also { session ->
|
||||||
@ -56,7 +56,7 @@ object ContentUploadStateTrackerBinder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
|
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) {
|
override fun onUpdate(state: ContentUploadStateTracker.State) {
|
||||||
when (state) {
|
when (state) {
|
||||||
|
@ -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.
|
// 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,
|
// 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.
|
// but first we check if we are waiting for the previous load to finish.
|
||||||
|
|
||||||
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
||||||
val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
|
val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
|
||||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||||
val totalItemCount = layoutManager.itemCount
|
val totalItemCount = layoutManager.itemCount
|
||||||
|
|
||||||
// The minimum amount of items to have below your current scroll position
|
// We check to see if the dataset count has
|
||||||
// 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
|
|
||||||
// changed, if so we conclude it has finished loading
|
// changed, if so we conclude it has finished loading
|
||||||
if (totalItemCount > previousTotalItemCount) {
|
if (totalItemCount != previousTotalItemCount) {
|
||||||
|
previousTotalItemCount = totalItemCount
|
||||||
loadingBackwards = false
|
loadingBackwards = false
|
||||||
loadingForwards = false
|
loadingForwards = false
|
||||||
previousTotalItemCount = totalItemCount
|
|
||||||
}
|
}
|
||||||
// If it isn’t currently loading, we check to see if we have reached
|
// If it isn’t currently loading, we check to see if we have reached
|
||||||
// the visibleThreshold and need to reload more data.
|
// the visibleThreshold and need to reload more data.
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.riotredesign.core.extensions.localDateTime
|
||||||
|
|
||||||
object TimelineDisplayableEvents {
|
object TimelineDisplayableEvents {
|
||||||
|
|
||||||
@ -48,8 +49,48 @@ fun List<TimelineEvent>.filterDisplayableEvents(): List<TimelineEvent> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun TimelineEvent.canBeMerged(): Boolean {
|
||||||
|
return root.type == EventType.STATE_ROOM_MEMBER
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<TimelineEvent>.nextSameTypeEvents(index: Int, minSize: Int): List<TimelineEvent> {
|
||||||
|
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<TimelineEvent>.prevSameTypeEvents(index: Int, minSize: Int): List<TimelineEvent> {
|
||||||
|
val prevSub = subList(0, index + 1)
|
||||||
|
return prevSub
|
||||||
|
.reversed()
|
||||||
|
.nextSameTypeEvents(0, minSize)
|
||||||
|
.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
fun List<TimelineEvent>.nextDisplayableEvent(index: Int): TimelineEvent? {
|
fun List<TimelineEvent>.nextDisplayableEvent(index: Int): TimelineEvent? {
|
||||||
return if (index == size - 1) {
|
return if (index >= size - 1) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
subList(index + 1, this.size).firstOrNull { it.isDisplayable() }
|
subList(index + 1, this.size).firstOrNull { it.isDisplayable() }
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -35,7 +35,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : VectorEpoxyModel<H>()
|
|||||||
holder.timeView.visibility = View.VISIBLE
|
holder.timeView.visibility = View.VISIBLE
|
||||||
holder.timeView.text = informationData.time
|
holder.timeView.text = informationData.time
|
||||||
holder.memberNameView.text = informationData.memberName
|
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 {
|
} else {
|
||||||
holder.avatarImageView.visibility = View.GONE
|
holder.avatarImageView.visibility = View.GONE
|
||||||
holder.memberNameView.visibility = View.GONE
|
holder.memberNameView.visibility = View.GONE
|
||||||
@ -43,6 +43,11 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : VectorEpoxyModel<H>()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun View.renderSendState() {
|
||||||
|
isClickable = informationData.sendState.isSent()
|
||||||
|
alpha = if (informationData.sendState.isSent()) 1f else 0.5f
|
||||||
|
}
|
||||||
|
|
||||||
abstract class Holder : VectorEpoxyHolder() {
|
abstract class Holder : VectorEpoxyHolder() {
|
||||||
abstract val avatarImageView: ImageView
|
abstract val avatarImageView: ImageView
|
||||||
abstract val memberNameView: TextView
|
abstract val memberNameView: TextView
|
||||||
|
@ -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<Data>,
|
||||||
|
private val onCollapsedStateChanged: (Boolean) -> Unit
|
||||||
|
) : VectorEpoxyModel<MergedHeaderItem.Holder>() {
|
||||||
|
|
||||||
|
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<TextView>(R.id.itemMergedExpandTextView)
|
||||||
|
val summaryView by bind<TextView>(R.id.itemMergedSummaryTextView)
|
||||||
|
val separatorView by bind<View>(R.id.itemMergedSeparatorView)
|
||||||
|
val avatarListView by bind<ViewGroup>(R.id.itemMergedAvatarListView)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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<MessageFileItem.Holder>() {
|
||||||
|
|
||||||
|
@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<ImageView>(R.id.messageAvatarImageView)
|
||||||
|
override val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||||
|
override val timeView by bind<TextView>(R.id.messageTimeView)
|
||||||
|
val fileLayout by bind<ViewGroup>(R.id.messageFileLayout)
|
||||||
|
val fileImageView by bind<ImageView>(R.id.messageFileImageView)
|
||||||
|
val filenameView by bind<TextView>(R.id.messageFilenameView)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -24,27 +24,27 @@ import com.airbnb.epoxy.EpoxyAttribute
|
|||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
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)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_image_video_message)
|
||||||
abstract class MessageImageItem : AbsMessageItem<MessageImageItem.Holder>() {
|
abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Holder>() {
|
||||||
|
|
||||||
@EpoxyAttribute lateinit var mediaData: MediaContentRenderer.Data
|
@EpoxyAttribute lateinit var mediaData: ImageContentRenderer.Data
|
||||||
@EpoxyAttribute lateinit var eventId: String
|
|
||||||
@EpoxyAttribute override lateinit var informationData: MessageInformationData
|
@EpoxyAttribute override lateinit var informationData: MessageInformationData
|
||||||
|
@EpoxyAttribute var playable: Boolean = false
|
||||||
@EpoxyAttribute var clickListener: View.OnClickListener? = null
|
@EpoxyAttribute var clickListener: View.OnClickListener? = null
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
MediaContentRenderer.render(mediaData, MediaContentRenderer.Mode.THUMBNAIL, holder.imageView)
|
ImageContentRenderer.render(mediaData, ImageContentRenderer.Mode.THUMBNAIL, holder.imageView)
|
||||||
ContentUploadStateTrackerBinder.bind(eventId, mediaData, holder.progressLayout)
|
ContentUploadStateTrackerBinder.bind(informationData.eventId, mediaData, holder.progressLayout)
|
||||||
holder.imageView.setOnClickListener(clickListener)
|
holder.imageView.setOnClickListener(clickListener)
|
||||||
holder.imageView.isEnabled = !mediaData.isLocalFile()
|
holder.imageView.renderSendState()
|
||||||
holder.imageView.alpha = if (mediaData.isLocalFile()) 0.5f else 1f
|
holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unbind(holder: Holder) {
|
override fun unbind(holder: Holder) {
|
||||||
ContentUploadStateTrackerBinder.unbind(eventId)
|
ContentUploadStateTrackerBinder.unbind(informationData.eventId)
|
||||||
super.unbind(holder)
|
super.unbind(holder)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,8 +52,9 @@ abstract class MessageImageItem : AbsMessageItem<MessageImageItem.Holder>() {
|
|||||||
override val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
override val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
||||||
override val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
override val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||||
override val timeView by bind<TextView>(R.id.messageTimeView)
|
override val timeView by bind<TextView>(R.id.messageTimeView)
|
||||||
val progressLayout by bind<ViewGroup>(R.id.messageImageUploadProgressLayout)
|
val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout)
|
||||||
val imageView by bind<ImageView>(R.id.messageImageView)
|
val imageView by bind<ImageView>(R.id.messageThumbnailView)
|
||||||
|
val playContentView by bind<ImageView>(R.id.messageMediaPlayView)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -16,7 +16,12 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
|
|
||||||
data class MessageInformationData(
|
data class MessageInformationData(
|
||||||
|
val eventId: String,
|
||||||
|
val senderId: String,
|
||||||
|
val sendState: SendState,
|
||||||
val time: CharSequence? = null,
|
val time: CharSequence? = null,
|
||||||
val avatarUrl: String?,
|
val avatarUrl: String?,
|
||||||
val memberName: CharSequence? = null,
|
val memberName: CharSequence? = null,
|
||||||
|
@ -45,6 +45,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
|||||||
TextViewCompat.getTextMetricsParams(holder.messageView),
|
TextViewCompat.getTextMetricsParams(holder.messageView),
|
||||||
null)
|
null)
|
||||||
holder.messageView.setTextFuture(textFuture)
|
holder.messageView.setTextFuture(textFuture)
|
||||||
|
holder.messageView.renderSendState()
|
||||||
findPillsAndProcess { it.bind(holder.messageView) }
|
findPillsAndProcess { it.bind(holder.messageView) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,11 +30,12 @@ abstract class NoticeItem : VectorEpoxyModel<NoticeItem.Holder>() {
|
|||||||
|
|
||||||
@EpoxyAttribute var noticeText: CharSequence? = null
|
@EpoxyAttribute var noticeText: CharSequence? = null
|
||||||
@EpoxyAttribute var avatarUrl: String? = null
|
@EpoxyAttribute var avatarUrl: String? = null
|
||||||
|
@EpoxyAttribute var userId: String = ""
|
||||||
@EpoxyAttribute var memberName: CharSequence? = null
|
@EpoxyAttribute var memberName: CharSequence? = null
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
holder.noticeTextView.text = noticeText
|
holder.noticeTextView.text = noticeText
|
||||||
AvatarRenderer.render(avatarUrl, memberName?.toString(), holder.avatarImageView)
|
AvatarRenderer.render(avatarUrl, userId, memberName?.toString(), holder.avatarImageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Holder : VectorEpoxyHolder() {
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
@ -79,6 +79,7 @@ class RoomSummaryController(private val stringProvider: StringProvider
|
|||||||
|
|
||||||
roomSummaryItem {
|
roomSummaryItem {
|
||||||
id(roomSummary.roomId)
|
id(roomSummary.roomId)
|
||||||
|
roomId(roomSummary.roomId)
|
||||||
roomName(roomSummary.displayName)
|
roomName(roomSummary.displayName)
|
||||||
avatarUrl(roomSummary.avatarUrl)
|
avatarUrl(roomSummary.avatarUrl)
|
||||||
selected(isSelected)
|
selected(isSelected)
|
||||||
|
@ -31,6 +31,7 @@ import im.vector.riotredesign.features.home.AvatarRenderer
|
|||||||
abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
||||||
|
|
||||||
@EpoxyAttribute lateinit var roomName: CharSequence
|
@EpoxyAttribute lateinit var roomName: CharSequence
|
||||||
|
@EpoxyAttribute lateinit var roomId: String
|
||||||
@EpoxyAttribute var avatarUrl: String? = null
|
@EpoxyAttribute var avatarUrl: String? = null
|
||||||
@EpoxyAttribute var selected: Boolean = false
|
@EpoxyAttribute var selected: Boolean = false
|
||||||
@EpoxyAttribute var unreadCount: Int = 0
|
@EpoxyAttribute var unreadCount: Int = 0
|
||||||
@ -44,7 +45,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
|||||||
holder.rootView.isChecked = selected
|
holder.rootView.isChecked = selected
|
||||||
holder.rootView.setOnClickListener { listener?.invoke() }
|
holder.rootView.setOnClickListener { listener?.invoke() }
|
||||||
holder.titleView.text = roomName
|
holder.titleView.text = roomName
|
||||||
AvatarRenderer.render(avatarUrl, roomName.toString(), holder.avatarImageView)
|
AvatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
class Holder : VectorEpoxyHolder() {
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
@ -52,7 +52,7 @@ class PillImageSpan(private val glideRequests: GlideRequests,
|
|||||||
@UiThread
|
@UiThread
|
||||||
fun bind(textView: TextView) {
|
fun bind(textView: TextView) {
|
||||||
tv = WeakReference(textView)
|
tv = WeakReference(textView)
|
||||||
AvatarRenderer.render(context, glideRequests, user?.avatarUrl, displayName, target)
|
AvatarRenderer.render(context, glideRequests, user?.avatarUrl, userId, displayName, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplacementSpan *****************************************************************************
|
// ReplacementSpan *****************************************************************************
|
||||||
@ -105,7 +105,7 @@ class PillImageSpan(private val glideRequests: GlideRequests,
|
|||||||
textStartPadding = textPadding
|
textStartPadding = textPadding
|
||||||
setChipMinHeightResource(R.dimen.pill_min_height)
|
setChipMinHeightResource(R.dimen.pill_min_height)
|
||||||
setChipIconSizeResource(R.dimen.pill_avatar_size)
|
setChipIconSizeResource(R.dimen.pill_avatar_size)
|
||||||
chipIcon = AvatarRenderer.getPlaceholderDrawable(context, displayName)
|
chipIcon = AvatarRenderer.getPlaceholderDrawable(context, userId, displayName)
|
||||||
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
|
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ import im.vector.riotredesign.core.glide.GlideApp
|
|||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
object MediaContentRenderer {
|
object ImageContentRenderer {
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Data(
|
data class Data(
|
||||||
@ -37,8 +37,8 @@ object MediaContentRenderer {
|
|||||||
val maxHeight: Int,
|
val maxHeight: Int,
|
||||||
val width: Int?,
|
val width: Int?,
|
||||||
val maxWidth: Int,
|
val maxWidth: Int,
|
||||||
val orientation: Int?,
|
val orientation: Int? = null,
|
||||||
val rotation: Int?
|
val rotation: Int? = null
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
fun isLocalFile(): Boolean {
|
fun isLocalFile(): Boolean {
|
||||||
@ -66,6 +66,7 @@ object MediaContentRenderer {
|
|||||||
GlideApp
|
GlideApp
|
||||||
.with(imageView)
|
.with(imageView)
|
||||||
.load(resolvedUrl)
|
.load(resolvedUrl)
|
||||||
|
.dontAnimate()
|
||||||
.thumbnail(0.3f)
|
.thumbnail(0.3f)
|
||||||
.into(imageView)
|
.into(imageView)
|
||||||
}
|
}
|
||||||
@ -73,9 +74,6 @@ object MediaContentRenderer {
|
|||||||
fun render(data: Data, imageView: BigImageView) {
|
fun render(data: Data, imageView: BigImageView) {
|
||||||
val (width, height) = processSize(data, Mode.THUMBNAIL)
|
val (width, height) = processSize(data, Mode.THUMBNAIL)
|
||||||
val contentUrlResolver = Matrix.getInstance().currentSession!!.contentUrlResolver()
|
val contentUrlResolver = Matrix.getInstance().currentSession!!.contentUrlResolver()
|
||||||
if (data.isLocalFile()) {
|
|
||||||
imageView.showImage(Uri.parse(data.url))
|
|
||||||
} else {
|
|
||||||
val fullSize = contentUrlResolver.resolveFullSize(data.url)
|
val fullSize = contentUrlResolver.resolveFullSize(data.url)
|
||||||
val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||||
imageView.showImage(
|
imageView.showImage(
|
||||||
@ -83,7 +81,6 @@ object MediaContentRenderer {
|
|||||||
Uri.parse(fullSize)
|
Uri.parse(fullSize)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun processSize(data: Data, mode: Mode): Pair<Int, Int> {
|
private fun processSize(data: Data, mode: Mode): Pair<Int, Int> {
|
||||||
val maxImageWidth = data.maxWidth
|
val maxImageWidth = data.maxWidth
|
@ -25,26 +25,26 @@ import androidx.appcompat.widget.Toolbar
|
|||||||
import com.github.piasy.biv.indicator.progresspie.ProgressPieIndicator
|
import com.github.piasy.biv.indicator.progresspie.ProgressPieIndicator
|
||||||
import com.github.piasy.biv.view.GlideImageViewFactory
|
import com.github.piasy.biv.view.GlideImageViewFactory
|
||||||
import im.vector.riotredesign.core.platform.VectorBaseActivity
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(im.vector.riotredesign.R.layout.activity_media_viewer)
|
setContentView(im.vector.riotredesign.R.layout.activity_image_media_viewer)
|
||||||
val mediaData = intent.getParcelableExtra<MediaContentRenderer.Data>(EXTRA_MEDIA_DATA)
|
val mediaData = intent.getParcelableExtra<ImageContentRenderer.Data>(EXTRA_MEDIA_DATA)
|
||||||
if (mediaData.url.isNullOrEmpty()) {
|
if (mediaData.url.isNullOrEmpty()) {
|
||||||
finish()
|
finish()
|
||||||
} else {
|
} else {
|
||||||
configureToolbar(mediaViewerToolbar, mediaData)
|
configureToolbar(imageMediaViewerToolbar, mediaData)
|
||||||
mediaViewerImageView.setImageViewFactory(GlideImageViewFactory())
|
imageMediaViewerImageView.setImageViewFactory(GlideImageViewFactory())
|
||||||
mediaViewerImageView.setProgressIndicator(ProgressPieIndicator())
|
imageMediaViewerImageView.setProgressIndicator(ProgressPieIndicator())
|
||||||
MediaContentRenderer.render(mediaData, mediaViewerImageView)
|
ImageContentRenderer.render(mediaData, imageMediaViewerImageView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun configureToolbar(toolbar: Toolbar, mediaData: MediaContentRenderer.Data) {
|
private fun configureToolbar(toolbar: Toolbar, mediaData: ImageContentRenderer.Data) {
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
supportActionBar?.apply {
|
supportActionBar?.apply {
|
||||||
title = mediaData.filename
|
title = mediaData.filename
|
||||||
@ -57,8 +57,8 @@ class MediaViewerActivity : VectorBaseActivity() {
|
|||||||
|
|
||||||
private const val EXTRA_MEDIA_DATA = "EXTRA_MEDIA_DATA"
|
private const val EXTRA_MEDIA_DATA = "EXTRA_MEDIA_DATA"
|
||||||
|
|
||||||
fun newIntent(context: Context, mediaData: MediaContentRenderer.Data): Intent {
|
fun newIntent(context: Context, mediaData: ImageContentRenderer.Data): Intent {
|
||||||
return Intent(context, MediaViewerActivity::class.java).apply {
|
return Intent(context, ImageMediaViewerActivity::class.java).apply {
|
||||||
putExtra(EXTRA_MEDIA_DATA, mediaData)
|
putExtra(EXTRA_MEDIA_DATA, mediaData)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<VideoContentRenderer.Data>(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -6,7 +6,7 @@
|
|||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
<androidx.appcompat.widget.Toolbar
|
||||||
android:id="@+id/mediaViewerToolbar"
|
android:id="@+id/imageMediaViewerToolbar"
|
||||||
style="@style/VectorToolbarStyle"
|
style="@style/VectorToolbarStyle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="?attr/actionBarSize"
|
||||||
@ -15,7 +15,7 @@
|
|||||||
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
|
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
|
||||||
|
|
||||||
<com.github.piasy.biv.view.BigImageView
|
<com.github.piasy.biv.view.BigImageView
|
||||||
android:id="@+id/mediaViewerImageView"
|
android:id="@+id/imageMediaViewerImageView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:failureImageInitScaleType="center"
|
app:failureImageInitScaleType="center"
|
48
vector/src/main/res/layout/activity_video_media_viewer.xml
Normal file
48
vector/src/main/res/layout/activity_video_media_viewer.xml
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/videoMediaViewerToolbar"
|
||||||
|
style="@style/VectorToolbarStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="?attr/colorPrimary"
|
||||||
|
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
|
||||||
|
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/videoMediaViewerThumbnailView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<VideoView
|
||||||
|
android:id="@+id/videoMediaViewerVideoView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
135
vector/src/main/res/layout/item_timeline_event_file_message.xml
Normal file
135
vector/src/main/res/layout/item_timeline_event_file_message.xml
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingRight="16dp">
|
||||||
|
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/messageAvatarImageView"
|
||||||
|
android:layout_width="@dimen/chat_avatar_size"
|
||||||
|
android:layout_height="@dimen/chat_avatar_size"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/messageMemberNameView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="64dp"
|
||||||
|
android:layout_marginLeft="64dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textSize="15sp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/messageTimeView"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="@tools:sample/full_names" />
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/messageTimeView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:duplicateParentState="true"
|
||||||
|
android:textColor="@color/brown_grey"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/messageMemberNameView"
|
||||||
|
tools:text="@tools:sample/date/hhmm" />
|
||||||
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/messageFileLayout"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="64dp"
|
||||||
|
android:layout_marginLeft="64dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="32dp"
|
||||||
|
android:layout_marginRight="32dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:duplicateParentState="true"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/messageMemberNameView">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/messageFilee2eIcon"
|
||||||
|
android:layout_width="14dp"
|
||||||
|
android:layout_height="14dp"
|
||||||
|
android:src="@drawable/e2e_verified"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<!-- the media type -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/messageFileImageView"
|
||||||
|
android:layout_width="@dimen/chat_avatar_size"
|
||||||
|
android:layout_height="@dimen/chat_avatar_size"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:layout_marginLeft="4dp"
|
||||||
|
android:src="@drawable/filetype_image" />
|
||||||
|
|
||||||
|
<!-- the media -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/messageFilenameView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="@dimen/chat_avatar_size"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:layout_marginLeft="4dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:autoLink="none"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
tools:text="A filename here" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/messageMediaUploadProgressLayout"
|
||||||
|
layout="@layout/media_upload_download_progress_layout"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="46dp"
|
||||||
|
android:layout_marginStart="64dp"
|
||||||
|
android:layout_marginLeft="64dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="32dp"
|
||||||
|
android:layout_marginRight="32dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/messageFileLayout"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,4 +1,19 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
@ -43,6 +58,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
|
android:duplicateParentState="true"
|
||||||
android:textColor="@color/brown_grey"
|
android:textColor="@color/brown_grey"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
@ -50,7 +66,7 @@
|
|||||||
tools:text="@tools:sample/date/hhmm" />
|
tools:text="@tools:sample/date/hhmm" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/messageImageView"
|
android:id="@+id/messageThumbnailView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginStart="64dp"
|
android:layout_marginStart="64dp"
|
||||||
@ -59,13 +75,28 @@
|
|||||||
android:layout_marginEnd="32dp"
|
android:layout_marginEnd="32dp"
|
||||||
android:layout_marginRight="32dp"
|
android:layout_marginRight="32dp"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
|
android:duplicateParentState="true"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0"
|
app:layout_constraintHorizontal_bias="0"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/messageMemberNameView" />
|
app:layout_constraintTop_toBottomOf="@+id/messageMemberNameView"
|
||||||
|
tools:layout_height="300dp" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/messageMediaPlayView"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:src="@drawable/ic_material_play_circle"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/messageThumbnailView"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/messageThumbnailView"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/messageThumbnailView"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/messageThumbnailView"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
||||||
<include
|
<include
|
||||||
android:id="@+id/messageImageUploadProgressLayout"
|
android:id="@+id/messageMediaUploadProgressLayout"
|
||||||
layout="@layout/media_upload_download_progress_layout"
|
layout="@layout/media_upload_download_progress_layout"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="46dp"
|
android:layout_height="46dp"
|
||||||
@ -78,7 +109,7 @@
|
|||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/messageImageView"
|
app:layout_constraintTop_toBottomOf="@+id/messageThumbnailView"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingRight="16dp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/itemMergedAvatarListView"
|
||||||
|
layout="@layout/vector_message_merge_avatar_list"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="64dp"
|
||||||
|
android:layout_marginLeft="64dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginRight="16dp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/itemMergedExpandTextView"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/itemMergedExpandTextView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingRight="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:paddingLeft="8dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingBottom="4dp"
|
||||||
|
android:text="@string/merged_events_expand"
|
||||||
|
android:textColor="?attr/colorAccent"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="italic"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/itemMergedSeparatorView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:background="?attr/colorAccent"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/itemMergedExpandTextView"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/itemMergedAvatarListView"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/itemMergedExpandTextView" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/itemMergedSummaryTextView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:textColor="?android:textColorSecondary"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/itemMergedAvatarListView"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/itemMergedSeparatorView"
|
||||||
|
tools:text="3 membership changes" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -44,6 +44,7 @@
|
|||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
android:textColor="@color/brown_grey"
|
android:textColor="@color/brown_grey"
|
||||||
|
android:duplicateParentState="true"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
app:layout_constraintTop_toTopOf="@id/messageMemberNameView"
|
app:layout_constraintTop_toTopOf="@id/messageMemberNameView"
|
||||||
@ -56,6 +57,7 @@
|
|||||||
android:layout_marginStart="64dp"
|
android:layout_marginStart="64dp"
|
||||||
android:layout_marginLeft="64dp"
|
android:layout_marginLeft="64dp"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
|
android:duplicateParentState="true"
|
||||||
android:textColor="@color/dark_grey"
|
android:textColor="@color/dark_grey"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/mels_list_avatar_1"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/mels_list_avatar_2"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:layout_marginStart="-5dp"
|
||||||
|
android:layout_marginLeft="-5dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/mels_list_avatar_3"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:layout_marginStart="-5dp"
|
||||||
|
android:layout_marginLeft="-5dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/mels_list_avatar_4"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:layout_marginStart="-5dp"
|
||||||
|
android:layout_marginLeft="-5dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/mels_list_avatar_5"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:layout_marginStart="-5dp"
|
||||||
|
android:layout_marginLeft="-5dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
Loading…
Reference in New Issue
Block a user