mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-21 18:26:57 +00:00
Add polish to stories link previews.
This commit is contained in:
committed by
Greyson Parrelli
parent
19af68a27c
commit
c7cd261641
@@ -0,0 +1,35 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.util.AttributeSet
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.graphics.withClip
|
||||
|
||||
/**
|
||||
* Adds manual clipping around the card. This ensures that software rendering
|
||||
* still maintains border radius of cards.
|
||||
*/
|
||||
class ClippedCardView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : CardView(context, attrs) {
|
||||
|
||||
private val bounds = Rect()
|
||||
private val boundsF = RectF()
|
||||
private val path = Path()
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
canvas.getClipBounds(bounds)
|
||||
boundsF.set(bounds)
|
||||
path.reset()
|
||||
|
||||
path.addRoundRect(boundsF, radius, radius, Path.Direction.CW)
|
||||
canvas.withClip(path) {
|
||||
super.draw(canvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
@@ -14,6 +15,7 @@ import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Px;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
@@ -280,6 +282,14 @@ public class ThumbnailView extends FrameLayout {
|
||||
forceLayout();
|
||||
}
|
||||
|
||||
public void setImageDrawable(@NonNull GlideRequests glideRequests, @Nullable Drawable drawable) {
|
||||
glideRequests.clear(image);
|
||||
glideRequests.clear(blurhash);
|
||||
|
||||
image.setImageDrawable(drawable);
|
||||
blurhash.setImageDrawable(null);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide,
|
||||
boolean showControls, boolean isPreview)
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
import org.thoughtcrime.securesms.stories.StoryTextPostView
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creation_fragment), TextStoryPostTextEntryFragment.Callback, SafetyNumberBottomSheet.Callbacks {
|
||||
@@ -102,8 +103,10 @@ class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creati
|
||||
send.isEnabled = canSend
|
||||
}
|
||||
|
||||
linkPreviewViewModel.linkPreviewState.observe(viewLifecycleOwner) { state ->
|
||||
storyTextPostView.bindLinkPreviewState(state, View.GONE)
|
||||
LiveDataUtil.combineLatest(viewModel.state, linkPreviewViewModel.linkPreviewState) { viewState, linkState ->
|
||||
Pair(viewState.body.isBlank(), linkState)
|
||||
}.observe(viewLifecycleOwner) { (useLargeThumb, linkState) ->
|
||||
storyTextPostView.bindLinkPreviewState(linkState, View.GONE, useLargeThumb)
|
||||
storyTextPostView.postAdjustLinkPreviewTranslationY()
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ class TextStoryPostLinkEntryFragment : KeyboardEntryDialogFragment(
|
||||
}
|
||||
|
||||
linkPreviewViewModel.linkPreviewState.observe(viewLifecycleOwner) { state ->
|
||||
linkPreview.bind(state)
|
||||
linkPreview.bind(state, useLargeThumbnail = false)
|
||||
shareALinkGroup.visible = !state.isLoading && !state.linkPreview.isPresent && (state.error == null && state.activeUrlForError == null)
|
||||
confirmButton.isEnabled = state.linkPreview.isPresent || state.activeUrlForError != null
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
package org.thoughtcrime.securesms.stories
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.OutlinedThumbnailView
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||
import org.thoughtcrime.securesms.mms.Slide
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture
|
||||
import org.thoughtcrime.securesms.util.views.Stub
|
||||
@@ -32,26 +33,54 @@ class StoryLinkPreviewView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
private val close: View = findViewById(R.id.link_preview_close)
|
||||
private val image: OutlinedThumbnailView = findViewById(R.id.link_preview_image)
|
||||
private val smallImage: ThumbnailView = findViewById(R.id.link_preview_image)
|
||||
private val largeImage: ThumbnailView = findViewById(R.id.link_preview_large)
|
||||
private val title: TextView = findViewById(R.id.link_preview_title)
|
||||
private val url: TextView = findViewById(R.id.link_preview_url)
|
||||
private val description: TextView = findViewById(R.id.link_preview_description)
|
||||
private val fallbackIcon: ImageView = findViewById(R.id.link_preview_fallback_icon)
|
||||
private val loadingSpinner: Stub<View> = Stub(findViewById(R.id.loading_spinner))
|
||||
|
||||
fun bind(linkPreview: LinkPreview?, hiddenVisibility: Int = View.INVISIBLE): ListenableFuture<Boolean> {
|
||||
var listenableFuture: ListenableFuture<Boolean>? = null
|
||||
private fun getThumbnailTarget(useLargeThumbnail: Boolean): ThumbnailView {
|
||||
return if (useLargeThumbnail) largeImage else smallImage
|
||||
}
|
||||
|
||||
fun getThumbnailViewWidth(useLargeThumbnail: Boolean): Int {
|
||||
return getThumbnailTarget(useLargeThumbnail).measuredWidth
|
||||
}
|
||||
|
||||
fun getThumbnailViewHeight(useLargeThumbnail: Boolean): Int {
|
||||
return getThumbnailTarget(useLargeThumbnail).measuredHeight
|
||||
}
|
||||
|
||||
fun setThumbnailDrawable(drawable: Drawable?, useLargeThumbnail: Boolean) {
|
||||
val image = getThumbnailTarget(useLargeThumbnail)
|
||||
image.setImageDrawable(GlideApp.with(this), drawable)
|
||||
}
|
||||
|
||||
fun bind(linkPreview: LinkPreview?, hiddenVisibility: Int = View.INVISIBLE, useLargeThumbnail: Boolean, loadThumbnail: Boolean = true): ListenableFuture<Boolean> {
|
||||
var future: ListenableFuture<Boolean>? = null
|
||||
|
||||
if (linkPreview != null) {
|
||||
visibility = View.VISIBLE
|
||||
isClickable = true
|
||||
|
||||
val corners = DimensionUnit.DP.toPixels(18f).toInt()
|
||||
image.setCorners(corners, corners, corners, corners)
|
||||
val image = getThumbnailTarget(useLargeThumbnail)
|
||||
val notImage = getThumbnailTarget(!useLargeThumbnail)
|
||||
|
||||
val imageSlide: ImageSlide? = linkPreview.thumbnail.map { ImageSlide(image.context, it) }.orElse(null)
|
||||
notImage.visible = false
|
||||
|
||||
val imageSlide: Slide? = linkPreview.thumbnail.map { ImageSlide(context, it) }.orElse(null)
|
||||
if (imageSlide != null) {
|
||||
listenableFuture = image.setImageResource(GlideApp.with(image), imageSlide, false, false)
|
||||
if (loadThumbnail) {
|
||||
future = image.setImageResource(
|
||||
GlideApp.with(this),
|
||||
imageSlide,
|
||||
false,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
image.visible = true
|
||||
fallbackIcon.visible = false
|
||||
} else {
|
||||
@@ -69,17 +98,17 @@ class StoryLinkPreviewView @JvmOverloads constructor(
|
||||
isClickable = false
|
||||
}
|
||||
|
||||
return listenableFuture ?: SettableFuture(false)
|
||||
return future ?: SettableFuture(false)
|
||||
}
|
||||
|
||||
fun bind(linkPreviewState: LinkPreviewViewModel.LinkPreviewState, hiddenVisibility: Int = View.INVISIBLE) {
|
||||
fun bind(linkPreviewState: LinkPreviewViewModel.LinkPreviewState, hiddenVisibility: Int = View.INVISIBLE, useLargeThumbnail: Boolean) {
|
||||
val linkPreview: LinkPreview? = linkPreviewState.linkPreview.orElseGet {
|
||||
linkPreviewState.activeUrlForError?.let {
|
||||
LinkPreview(it, LinkPreviewUtil.getTopLevelDomain(it) ?: it, null, -1L, null)
|
||||
}
|
||||
}
|
||||
|
||||
bind(linkPreview, hiddenVisibility)
|
||||
bind(linkPreview, hiddenVisibility, useLargeThumbnail)
|
||||
|
||||
loadingSpinner.get().visible = linkPreviewState.isLoading
|
||||
if (linkPreviewState.isLoading) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.View
|
||||
import androidx.core.graphics.scale
|
||||
import androidx.core.view.drawToBitmap
|
||||
@@ -14,6 +15,7 @@ import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.ResourceDecoder
|
||||
import com.bumptech.glide.load.engine.Resource
|
||||
import com.bumptech.glide.load.resource.SimpleResource
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
@@ -23,6 +25,8 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.fonts.TextFont
|
||||
import org.thoughtcrime.securesms.fonts.TextToScript
|
||||
import org.thoughtcrime.securesms.fonts.TypefaceCache
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
@@ -109,7 +113,7 @@ data class StoryTextPostModel(
|
||||
|
||||
override fun decode(source: StoryTextPostModel, width: Int, height: Int, options: Options): Resource<Bitmap> {
|
||||
val message = SignalDatabase.mmsSms.getMessageFor(source.storySentAtMillis, source.storyAuthor)
|
||||
val view = StoryTextPostView(ApplicationDependencies.getApplication())
|
||||
val view = StoryTextPostView(ContextThemeWrapper(ApplicationDependencies.getApplication(), R.style.TextSecure_DarkNoActionBar))
|
||||
val typeface = TypefaceCache.get(
|
||||
ApplicationDependencies.getApplication(),
|
||||
TextFont.fromStyle(source.storyTextPost.style),
|
||||
@@ -119,9 +123,30 @@ data class StoryTextPostModel(
|
||||
val displayWidth: Int = ApplicationDependencies.getApplication().resources.displayMetrics.widthPixels
|
||||
val arHeight: Int = (RENDER_HW_AR * displayWidth).toInt()
|
||||
|
||||
val linkPreview = (message as? MmsMessageRecord)?.linkPreviews?.firstOrNull()
|
||||
val useLargeThumbnail = source.text.isBlank()
|
||||
|
||||
view.setTypeface(typeface)
|
||||
view.bindFromStoryTextPost(source.storyTextPost)
|
||||
view.bindLinkPreview((message as? MmsMessageRecord)?.linkPreviews?.firstOrNull())
|
||||
view.bindLinkPreview(linkPreview, useLargeThumbnail, loadThumbnail = false)
|
||||
view.postAdjustLinkPreviewTranslationY()
|
||||
|
||||
view.invalidate()
|
||||
view.measure(View.MeasureSpec.makeMeasureSpec(displayWidth, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(arHeight, View.MeasureSpec.EXACTLY))
|
||||
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
|
||||
|
||||
val drawable = if (linkPreview != null && linkPreview.thumbnail.isPresent) {
|
||||
GlideApp
|
||||
.with(view)
|
||||
.load(DecryptableStreamUriLoader.DecryptableUri(linkPreview.thumbnail.get().uri!!))
|
||||
.centerCrop()
|
||||
.submit(view.getLinkPreviewThumbnailWidth(useLargeThumbnail), view.getLinkPreviewThumbnailHeight(useLargeThumbnail))
|
||||
.get()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
view.setLinkPreviewDrawable(drawable, useLargeThumbnail)
|
||||
|
||||
view.invalidate()
|
||||
view.measure(View.MeasureSpec.makeMeasureSpec(displayWidth, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(arHeight, View.MeasureSpec.EXACTLY))
|
||||
|
||||
@@ -48,6 +48,14 @@ class StoryTextPostView @JvmOverloads constructor(
|
||||
TextStoryTextWatcher.install(textView)
|
||||
}
|
||||
|
||||
fun getLinkPreviewThumbnailWidth(useLargeThumbnail: Boolean): Int {
|
||||
return linkPreviewView.getThumbnailViewWidth(useLargeThumbnail)
|
||||
}
|
||||
|
||||
fun getLinkPreviewThumbnailHeight(useLargeThumbnail: Boolean): Int {
|
||||
return linkPreviewView.getThumbnailViewHeight(useLargeThumbnail)
|
||||
}
|
||||
|
||||
fun showCloseButton() {
|
||||
linkPreviewView.setCanClose(true)
|
||||
}
|
||||
@@ -148,12 +156,16 @@ class StoryTextPostView @JvmOverloads constructor(
|
||||
postAdjustLinkPreviewTranslationY()
|
||||
}
|
||||
|
||||
fun bindLinkPreview(linkPreview: LinkPreview?): ListenableFuture<Boolean> {
|
||||
return linkPreviewView.bind(linkPreview, View.GONE)
|
||||
fun bindLinkPreview(linkPreview: LinkPreview?, useLargeThumbnail: Boolean, loadThumbnail: Boolean = true): ListenableFuture<Boolean> {
|
||||
return linkPreviewView.bind(linkPreview, View.GONE, useLargeThumbnail, loadThumbnail)
|
||||
}
|
||||
|
||||
fun bindLinkPreviewState(linkPreviewState: LinkPreviewViewModel.LinkPreviewState, hiddenVisibility: Int) {
|
||||
linkPreviewView.bind(linkPreviewState, hiddenVisibility)
|
||||
fun setLinkPreviewDrawable(drawable: Drawable?, useLargeThumbnail: Boolean) {
|
||||
linkPreviewView.setThumbnailDrawable(drawable, useLargeThumbnail)
|
||||
}
|
||||
|
||||
fun bindLinkPreviewState(linkPreviewState: LinkPreviewViewModel.LinkPreviewState, hiddenVisibility: Int, useLargeThumbnail: Boolean) {
|
||||
linkPreviewView.bind(linkPreviewState, hiddenVisibility, useLargeThumbnail)
|
||||
}
|
||||
|
||||
fun postAdjustLinkPreviewTranslationY() {
|
||||
@@ -186,7 +198,7 @@ class StoryTextPostView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
private fun canDisplayText(): Boolean {
|
||||
return !(linkPreviewView.isVisible && isPlaceholder)
|
||||
return !(linkPreviewView.isVisible && (isPlaceholder || textView.text.isEmpty()))
|
||||
}
|
||||
|
||||
private fun adjustLinkPreviewTranslationY() {
|
||||
|
||||
@@ -46,7 +46,8 @@ class StoryTextPostPreviewFragment : Fragment(R.layout.stories_text_post_preview
|
||||
StoryTextPostState.LoadState.INIT -> Unit
|
||||
StoryTextPostState.LoadState.LOADED -> {
|
||||
storyTextPostView.bindFromStoryTextPost(state.storyTextPost!!)
|
||||
storyTextPostView.bindLinkPreview(state.linkPreview)
|
||||
storyTextPostView.bindLinkPreview(state.linkPreview, state.storyTextPost.body.isBlank())
|
||||
storyTextPostView.postAdjustLinkPreviewTranslationY()
|
||||
|
||||
if (state.linkPreview != null) {
|
||||
storyTextPostView.setLinkPreviewClickListener {
|
||||
|
||||
Reference in New Issue
Block a user