2021-06-24 08:19:40 +02:00
|
|
|
package org.thoughtcrime.securesms.conversation.v2.utilities
|
2021-06-24 08:15:13 +02:00
|
|
|
|
|
|
|
import android.content.Context
|
|
|
|
import android.graphics.Bitmap
|
|
|
|
import android.graphics.drawable.Drawable
|
2021-06-25 01:57:58 +02:00
|
|
|
import android.net.Uri
|
2021-06-24 08:15:13 +02:00
|
|
|
import android.util.AttributeSet
|
2021-06-25 04:18:52 +02:00
|
|
|
import android.view.View
|
2021-06-24 08:15:13 +02:00
|
|
|
import android.widget.FrameLayout
|
|
|
|
import androidx.core.view.isVisible
|
|
|
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
2021-06-25 01:57:58 +02:00
|
|
|
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
|
|
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
2021-06-24 08:15:13 +02:00
|
|
|
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
|
|
|
import com.bumptech.glide.request.RequestOptions
|
|
|
|
import network.loki.messenger.R
|
2022-01-14 06:56:15 +01:00
|
|
|
import network.loki.messenger.databinding.ThumbnailViewBinding
|
2021-06-24 08:15:13 +02:00
|
|
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
|
|
|
import org.session.libsession.utilities.Util.equals
|
2021-06-25 01:57:58 +02:00
|
|
|
import org.session.libsignal.utilities.ListenableFuture
|
|
|
|
import org.session.libsignal.utilities.SettableFuture
|
|
|
|
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget
|
|
|
|
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget
|
2021-07-01 03:39:18 +02:00
|
|
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
2021-06-24 08:15:13 +02:00
|
|
|
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
|
2022-01-14 06:56:15 +01:00
|
|
|
import org.thoughtcrime.securesms.mms.GlideRequest
|
|
|
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
|
|
|
import org.thoughtcrime.securesms.mms.Slide
|
2023-01-13 03:22:18 +01:00
|
|
|
import kotlin.Boolean
|
|
|
|
import kotlin.Int
|
|
|
|
import kotlin.getValue
|
|
|
|
import kotlin.lazy
|
|
|
|
import kotlin.let
|
2021-06-24 08:15:13 +02:00
|
|
|
|
2023-01-13 03:22:18 +01:00
|
|
|
open class ThumbnailView: FrameLayout {
|
2021-06-24 08:15:13 +02:00
|
|
|
companion object {
|
|
|
|
private const val WIDTH = 0
|
|
|
|
private const val HEIGHT = 1
|
|
|
|
}
|
|
|
|
|
2023-01-13 03:22:18 +01:00
|
|
|
private val binding: ThumbnailViewBinding by lazy { ThumbnailViewBinding.bind(this) }
|
|
|
|
|
2021-06-24 08:15:13 +02:00
|
|
|
// region Lifecycle
|
|
|
|
constructor(context: Context) : super(context) { initialize(null) }
|
|
|
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) }
|
|
|
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
|
|
|
|
|
2022-01-14 06:56:15 +01:00
|
|
|
val loadIndicator: View by lazy { binding.thumbnailLoadIndicator }
|
2021-06-24 08:15:13 +02:00
|
|
|
|
|
|
|
private val dimensDelegate = ThumbnailDimensDelegate()
|
|
|
|
|
|
|
|
private var slide: Slide? = null
|
2023-01-13 03:22:18 +01:00
|
|
|
var radius: Int = 0
|
2021-06-24 08:15:13 +02:00
|
|
|
|
|
|
|
private fun initialize(attrs: AttributeSet?) {
|
|
|
|
if (attrs != null) {
|
|
|
|
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0)
|
|
|
|
|
2021-06-25 01:57:58 +02:00
|
|
|
dimensDelegate.setBounds(typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minWidth, 0),
|
|
|
|
typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0),
|
|
|
|
typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0),
|
|
|
|
typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0))
|
|
|
|
|
|
|
|
radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, 0)
|
2021-06-24 08:15:13 +02:00
|
|
|
|
|
|
|
typedArray.recycle()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
|
|
val adjustedDimens = dimensDelegate.resourceSize()
|
|
|
|
if (adjustedDimens[WIDTH] == 0 && adjustedDimens[HEIGHT] == 0) {
|
|
|
|
return super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
|
|
}
|
|
|
|
|
|
|
|
val finalWidth: Int = adjustedDimens[WIDTH] + paddingLeft + paddingRight
|
|
|
|
val finalHeight: Int = adjustedDimens[HEIGHT] + paddingTop + paddingBottom
|
|
|
|
|
|
|
|
super.onMeasure(
|
2023-01-13 03:22:18 +01:00
|
|
|
MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY),
|
|
|
|
MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY)
|
2021-06-24 08:15:13 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun getDefaultWidth() = maxOf(layoutParams?.width ?: 0, 0)
|
|
|
|
private fun getDefaultHeight() = maxOf(layoutParams?.height ?: 0, 0)
|
|
|
|
// endregion
|
|
|
|
|
|
|
|
// region Interaction
|
2023-01-13 03:22:18 +01:00
|
|
|
fun setImageResource(glide: GlideRequests, slide: Slide, isPreview: Boolean, mms: MmsMessageRecord?): ListenableFuture<Boolean> {
|
2021-07-01 03:39:18 +02:00
|
|
|
return setImageResource(glide, slide, isPreview, 0, 0, mms)
|
2021-06-24 08:15:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
fun setImageResource(glide: GlideRequests, slide: Slide,
|
2021-06-29 05:35:46 +02:00
|
|
|
isPreview: Boolean, naturalWidth: Int,
|
2023-01-13 03:22:18 +01:00
|
|
|
naturalHeight: Int, mms: MmsMessageRecord?): ListenableFuture<Boolean> {
|
2021-06-24 08:15:13 +02:00
|
|
|
|
|
|
|
val currentSlide = this.slide
|
|
|
|
|
2023-01-13 03:22:18 +01:00
|
|
|
binding.playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() &&
|
2021-06-24 08:15:13 +02:00
|
|
|
(slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
|
|
|
|
|
|
|
|
if (equals(currentSlide, slide)) {
|
|
|
|
// don't re-load slide
|
2021-06-25 01:57:58 +02:00
|
|
|
return SettableFuture(false)
|
2021-06-24 08:15:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (currentSlide != null && currentSlide.fastPreflightId != null && currentSlide.fastPreflightId == slide.fastPreflightId) {
|
|
|
|
// not reloading slide for fast preflight
|
|
|
|
this.slide = slide
|
|
|
|
}
|
|
|
|
|
|
|
|
this.slide = slide
|
|
|
|
|
2023-01-13 03:22:18 +01:00
|
|
|
binding.thumbnailLoadIndicator.isVisible = slide.isInProgress
|
|
|
|
binding.thumbnailDownloadIcon.isVisible = slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED
|
2021-06-24 08:15:13 +02:00
|
|
|
|
|
|
|
dimensDelegate.setDimens(naturalWidth, naturalHeight)
|
|
|
|
invalidate()
|
|
|
|
|
2021-06-25 01:57:58 +02:00
|
|
|
val result = SettableFuture<Boolean>()
|
|
|
|
|
2021-06-24 08:15:13 +02:00
|
|
|
when {
|
|
|
|
slide.thumbnailUri != null -> {
|
2023-02-10 04:33:19 +01:00
|
|
|
buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(binding.thumbnailImage, binding.thumbnailLoadIndicator, result))
|
2021-06-24 08:15:13 +02:00
|
|
|
}
|
|
|
|
slide.hasPlaceholder() -> {
|
2023-05-01 09:19:01 +02:00
|
|
|
buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(binding.thumbnailImage, null, result))
|
2021-06-24 08:15:13 +02:00
|
|
|
}
|
|
|
|
else -> {
|
2023-01-13 03:22:18 +01:00
|
|
|
glide.clear(binding.thumbnailImage)
|
2021-06-25 01:57:58 +02:00
|
|
|
result.set(false)
|
2021-06-24 08:15:13 +02:00
|
|
|
}
|
|
|
|
}
|
2021-06-25 01:57:58 +02:00
|
|
|
return result
|
2021-06-24 08:15:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
fun buildThumbnailGlideRequest(glide: GlideRequests, slide: Slide): GlideRequest<Drawable> {
|
|
|
|
|
|
|
|
val dimens = dimensDelegate.resourceSize()
|
|
|
|
|
|
|
|
val request = glide.load(DecryptableUri(slide.thumbnailUri!!))
|
Performance improvements and bug fixes (#869)
* refactor: fail on testSnode instead of recursively using up snode list. add call timeout on http client
* refactor: refactoring batch message receives and pollers
* refactor: reduce thread utils pool count to a 2 thread fixed pool. Do a check against pubkey instead of room names for oxenHostedOpenGroup
* refactor: caching lib with potential loader fixes and no-cache for giphy
* refactor: remove store and instead use ConcurrentHashMap with a backing update coroutine
* refactor: queue trim thread jobs instead of add every message processed
* fix: wrapping auth token and initial sync for open groups in a threadutils queued runnable, getting initial sync times down
* fix: fixing the user contacts cache in ConversationAdapter.kt
* refactor: improve polling and initial sync, move group joins from config messages into a background job fetching image.
* refactor: improving the job queuing for open groups, replacing placeholder avatar generation with a custom glide loader and archiving initial sync of open groups
* feat: add OpenGroupDeleteJob.kt
* feat: add open group delete job to process deletions after batch adding
* feat: add vacuum and fix job queue re-adding jobs forever, only try to set message hash values in DB if they have changed
* refactor: remove redundant inflation for profile image views throughout app
* refactor(wip): reducing layout inflation and starting to refactor the open group deletion issues taking a long time
* refactor(wip): refactoring group deletion to not iterate through and delete messages individually
* refactor(wip): refactoring group deletion to not iterate through and delete messages individually
* fix: group deletion optimisation
* build: bump build number
* build: bump build number and fix batch message receive retry logic
* fix: clear out open group deletes
* fix: update visible ConversationAdapter.kt binding for initial contact fetching and better traces for debugging background jobs
* fix: add in check for / force sync latest encryption key pair from linked devices if we already have that closed group
* Rename .java to .kt
* refactor: change MmsDatabase to kotlin to make list operations easier
* fix: nullable type
* fix: compilation issues and constants in .kt instead of .java
* fix: bug fix expiration timer on closed group recipient
* feat: use the job queue properly across executors
* feat: start on open group dispatcher-specific logic, probably a queue factory based on openGroupId if that is the same across new message and deletion jobs to ensure consistent entry and removal
* refactor: removing redundant code and fixing jobqueue per opengroup
* fix: allow attachments in note to self
* fix: make the minWidth in quote view bind max of text / title and body, wrapped ?
* fix: fixing up layouts and code view layouts
* fix: remove TODO, remove timestamp binding
* feat: fix view logic, avatars and padding, downloading attachments lazily (on bind), fixing potential crash, add WindowDebouncer.kt
* fix: NPE on viewModel recipient from removed thread while tearing down the Recipient observer in ConversationActivityV2.kt
* refactor: replace conversation notification debouncer handler with handlerthread, same as conversation list debouncer
* refactor: UI for groups and poller improvements
* fix: revert some changes in poller
* feat: add header back in for message requests
* refactor: remove Trace calls, add more conditions to the HomeDiffUtil for updating more efficiently
* feat: try update the home adapter if we get a profile picture modified event
* feat: bump build numbers
* fix: try to start with list in homeViewModel if we don't have already, render quotes to be width of attachment slide view instead of fixed
* fix: set channel to be conflated instead of no buffer
* fix: set unreads based off last local user message vs incrementing unreads to be all amount
* feat: add profile update flag, update build number
* fix: link preview thumbnails download on bind
* fix: centercrop placeholder in glide request
* feat: recycle the contact selection list and profile image in unbind
* fix: try to prevent user KP crash at weird times
* fix: remove additional log, improve attachment download success rate, fix share logs dialog issue
2022-06-08 09:12:34 +02:00
|
|
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
2021-06-24 08:15:13 +02:00
|
|
|
.let { request ->
|
|
|
|
if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) {
|
|
|
|
request.override(getDefaultWidth(), getDefaultHeight())
|
|
|
|
} else {
|
|
|
|
request.override(dimens[WIDTH], dimens[HEIGHT])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.transition(DrawableTransitionOptions.withCrossFade())
|
|
|
|
.centerCrop()
|
|
|
|
|
|
|
|
return if (slide.isInProgress) request else request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture))
|
|
|
|
}
|
|
|
|
|
|
|
|
fun buildPlaceholderGlideRequest(glide: GlideRequests, slide: Slide): GlideRequest<Bitmap> {
|
|
|
|
|
|
|
|
val dimens = dimensDelegate.resourceSize()
|
|
|
|
|
|
|
|
return glide.asBitmap()
|
|
|
|
.load(slide.getPlaceholderRes(context.theme))
|
|
|
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
|
|
.let { request ->
|
|
|
|
if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) {
|
|
|
|
request.override(getDefaultWidth(), getDefaultHeight())
|
|
|
|
} else {
|
|
|
|
request.override(dimens[WIDTH], dimens[HEIGHT])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.fitCenter()
|
|
|
|
}
|
2021-06-25 01:57:58 +02:00
|
|
|
|
|
|
|
open fun clear(glideRequests: GlideRequests) {
|
2023-01-13 03:22:18 +01:00
|
|
|
glideRequests.clear(binding.thumbnailImage)
|
2021-06-25 01:57:58 +02:00
|
|
|
slide = null
|
|
|
|
}
|
|
|
|
|
|
|
|
fun setImageResource(glideRequests: GlideRequests, uri: Uri): ListenableFuture<Boolean> {
|
|
|
|
val future = SettableFuture<Boolean>()
|
|
|
|
|
|
|
|
var request: GlideRequest<Drawable> = glideRequests.load(DecryptableUri(uri))
|
|
|
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
|
|
.transition(DrawableTransitionOptions.withCrossFade())
|
|
|
|
|
|
|
|
request = if (radius > 0) {
|
|
|
|
request.transforms(CenterCrop(), RoundedCorners(radius))
|
|
|
|
} else {
|
|
|
|
request.transforms(CenterCrop())
|
|
|
|
}
|
|
|
|
|
2023-02-10 04:33:19 +01:00
|
|
|
request.into(GlideDrawableListeningTarget(binding.thumbnailImage, binding.thumbnailLoadIndicator, future))
|
2021-06-25 01:57:58 +02:00
|
|
|
|
|
|
|
return future
|
|
|
|
}
|
2021-06-24 08:15:13 +02:00
|
|
|
}
|