fix: improves ConversationModel equals and hashCode to only contain fields important to the visual representation in the adapter

Signed-off-by: David Leibovych <ariedov@gmail.com>
This commit is contained in:
David Leibovych 2025-07-30 19:02:39 +03:00
parent 1a4c4bed75
commit c8aa4ddde4
2 changed files with 190 additions and 8 deletions

View File

@ -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<Drawable>(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)
}
}
}

View File

@ -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
}
}