From c8aa4ddde4bbf2c529bf2ec60cd316c769751c20 Mon Sep 17 00:00:00 2001 From: David Leibovych Date: Wed, 30 Jul 2025 19:02:39 +0300 Subject: [PATCH] fix: improves ConversationModel equals and hashCode to only contain fields important to the visual representation in the adapter Signed-off-by: David Leibovych --- .../talk/extensions/ImageViewExtensions.kt | 94 ++++++++++++++-- .../talk/models/domain/ConversationModel.kt | 104 ++++++++++++++++++ 2 files changed, 190 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt index 1044460e1..791cf41cb 100644 --- a/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt +++ b/app/src/main/java/com/nextcloud/talk/extensions/ImageViewExtensions.kt @@ -11,9 +11,18 @@ package com.nextcloud.talk.extensions import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapShader import android.graphics.Canvas import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.Matrix import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Shader +import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable import android.util.Log @@ -21,6 +30,7 @@ import android.widget.ImageView import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.createBitmap +import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toDrawable import coil.annotation.ExperimentalCoilApi import coil.imageLoader @@ -42,6 +52,7 @@ import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.TextDrawable import java.util.Locale +import kotlin.math.min private const val ROUNDING_PIXEL = 16f private const val TAG = "ImageViewExtensions" @@ -291,18 +302,12 @@ fun ImageView.loadSystemAvatar(): io.reactivex.disposables.Disposable { ) } -fun ImageView.loadNoteToSelfAvatar(): io.reactivex.disposables.Disposable { +fun ImageView.loadNoteToSelfAvatar() { val layers = arrayOfNulls(2) layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background) layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_note_to_self) val layerDrawable = LayerDrawable(layers) - val data: Any = layerDrawable - - return DisposableWrapper( - load(data) { - transformations(CircleCropTransformation()) - } - ) + setImageDrawable(CircularDrawable(layerDrawable)) } fun ImageView.loadFirstLetterAvatar(name: String): io.reactivex.disposables.Disposable { @@ -416,3 +421,76 @@ private class DisposableWrapper(private val disposable: coil.request.Disposable) override fun isDisposed(): Boolean = disposable.isDisposed } + +private class CircularDrawable(private val sourceDrawable: Drawable) : Drawable() { + + private val bitmap: Bitmap = drawableToBitmap(sourceDrawable) + private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) + } + + private val rect = RectF() + private var radius = 0f + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + rect.set(bounds) + + radius = min(rect.width() / 2.0f, rect.height() / 2.0f) + + val matrix = Matrix() + val scale: Float + var dx = 0f + var dy = 0f + + if (bitmap.width * rect.height() > rect.width() * bitmap.height) { + // Taller than wide, scale to height and center horizontally + scale = rect.height() / bitmap.height.toFloat() + dx = (rect.width() - bitmap.width * scale) * 0.5f + } else { + // Wider than tall, scale to width and center vertically + scale = rect.width() / bitmap.width.toFloat() + dy = (rect.height() - bitmap.height * scale) * 0.5f + } + + matrix.setScale(scale, scale) + matrix.postTranslate(dx.toInt().toFloat() + rect.left, dy.toInt().toFloat() + rect.top) + paint.shader.setLocalMatrix(matrix) + } + + override fun draw(canvas: Canvas) { + canvas.drawCircle(rect.centerX(), rect.centerY(), radius, paint) + } + + override fun setAlpha(alpha: Int) { + paint.alpha = alpha + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + paint.colorFilter = colorFilter + } + + @Deprecated("This method is no longer used in graphics optimizations", + ReplaceWith("PixelFormat.TRANSLUCENT", "android.graphics.PixelFormat") + ) + override fun getOpacity(): Int = PixelFormat.TRANSLUCENT + + override fun getIntrinsicWidth(): Int = sourceDrawable.intrinsicWidth + + override fun getIntrinsicHeight(): Int = sourceDrawable.intrinsicHeight + + companion object { + + private fun drawableToBitmap(drawable: Drawable): Bitmap { + if (drawable is BitmapDrawable) { + if (drawable.bitmap != null) { + return drawable.bitmap + } + } + + val width = if (drawable.intrinsicWidth > 0) drawable.intrinsicWidth else 1 + val height = if (drawable.intrinsicHeight > 0) drawable.intrinsicHeight else 1 + return drawable.toBitmap(width, height, Bitmap.Config.ARGB_8888) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt b/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt index c5ea387f0..896a288f7 100644 --- a/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt +++ b/app/src/main/java/com/nextcloud/talk/models/domain/ConversationModel.kt @@ -134,4 +134,108 @@ class ConversationModel( hasImportant = conversation.hasImportant ) } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ConversationModel + + if (hasPassword != other.hasPassword) return false + if (favorite != other.favorite) return false + if (lastActivity != other.lastActivity) return false + if (unreadMessages != other.unreadMessages) return false + if (unreadMention != other.unreadMention) return false + if (lobbyTimer != other.lobbyTimer) return false + if (lastReadMessage != other.lastReadMessage) return false + if (lastCommonReadMessage != other.lastCommonReadMessage) return false + if (hasCall != other.hasCall) return false + if (callFlag != other.callFlag) return false + if (canStartCall != other.canStartCall) return false + if (canLeaveConversation != other.canLeaveConversation) return false + if (canDeleteConversation != other.canDeleteConversation) return false + if (unreadMentionDirect != other.unreadMentionDirect) return false + if (notificationCalls != other.notificationCalls) return false + if (permissions != other.permissions) return false + if (messageExpiration != other.messageExpiration) return false + if (statusClearAt != other.statusClearAt) return false + if (callRecording != other.callRecording) return false + if (hasCustomAvatar != other.hasCustomAvatar) return false + if (callStartTime != other.callStartTime) return false + if (recordingConsentRequired != other.recordingConsentRequired) return false + if (hasArchived != other.hasArchived) return false + if (hasSensitive != other.hasSensitive) return false + if (hasImportant != other.hasImportant) return false + if (internalId != other.internalId) return false + if (name != other.name) return false + if (displayName != other.displayName) return false + if (description != other.description) return false + if (type != other.type) return false + if (participantType != other.participantType) return false + if (lastMessage != other.lastMessage) return false + if (objectType != other.objectType) return false + if (objectId != other.objectId) return false + if (notificationLevel != other.notificationLevel) return false + if (conversationReadOnlyState != other.conversationReadOnlyState) return false + if (lobbyState != other.lobbyState) return false + if (status != other.status) return false + if (statusIcon != other.statusIcon) return false + if (statusMessage != other.statusMessage) return false + if (avatarVersion != other.avatarVersion) return false + if (remoteServer != other.remoteServer) return false + if (remoteToken != other.remoteToken) return false + if (password != other.password) return false + if (messageDraft != other.messageDraft) return false + + return true + } + + override fun hashCode(): Int { + var result = hasPassword.hashCode() + result = 31 * result + favorite.hashCode() + result = 31 * result + lastActivity.hashCode() + result = 31 * result + unreadMessages + result = 31 * result + unreadMention.hashCode() + result = 31 * result + lobbyTimer.hashCode() + result = 31 * result + lastReadMessage + result = 31 * result + lastCommonReadMessage + result = 31 * result + hasCall.hashCode() + result = 31 * result + callFlag + result = 31 * result + canStartCall.hashCode() + result = 31 * result + canLeaveConversation.hashCode() + result = 31 * result + canDeleteConversation.hashCode() + result = 31 * result + unreadMentionDirect.hashCode() + result = 31 * result + notificationCalls + result = 31 * result + permissions + result = 31 * result + messageExpiration + result = 31 * result + (statusClearAt?.hashCode() ?: 0) + result = 31 * result + callRecording + result = 31 * result + hasCustomAvatar.hashCode() + result = 31 * result + callStartTime.hashCode() + result = 31 * result + recordingConsentRequired + result = 31 * result + hasArchived.hashCode() + result = 31 * result + hasSensitive.hashCode() + result = 31 * result + hasImportant.hashCode() + result = 31 * result + internalId.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + displayName.hashCode() + result = 31 * result + description.hashCode() + result = 31 * result + type.hashCode() + result = 31 * result + participantType.hashCode() + result = 31 * result + (lastMessage?.hashCode() ?: 0) + result = 31 * result + objectType.hashCode() + result = 31 * result + objectId.hashCode() + result = 31 * result + notificationLevel.hashCode() + result = 31 * result + conversationReadOnlyState.hashCode() + result = 31 * result + lobbyState.hashCode() + result = 31 * result + (status?.hashCode() ?: 0) + result = 31 * result + (statusIcon?.hashCode() ?: 0) + result = 31 * result + (statusMessage?.hashCode() ?: 0) + result = 31 * result + avatarVersion.hashCode() + result = 31 * result + (remoteServer?.hashCode() ?: 0) + result = 31 * result + (remoteToken?.hashCode() ?: 0) + result = 31 * result + (password?.hashCode() ?: 0) + result = 31 * result + (messageDraft?.hashCode() ?: 0) + return result + } }