diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ClippedCardView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ClippedCardView.kt new file mode 100644 index 0000000000..08effd2e88 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ClippedCardView.kt @@ -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) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java index f607eed6ef..fe9521c0b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java @@ -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 setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide, boolean showControls, boolean isPreview) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt index 2869a2f924..4539d9403f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt @@ -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() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostLinkEntryFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostLinkEntryFragment.kt index 971443a737..74f1cd5898 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostLinkEntryFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostLinkEntryFragment.kt @@ -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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryLinkPreviewView.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryLinkPreviewView.kt index 6084c4acde..febe2e4c95 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryLinkPreviewView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryLinkPreviewView.kt @@ -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 = Stub(findViewById(R.id.loading_spinner)) - fun bind(linkPreview: LinkPreview?, hiddenVisibility: Int = View.INVISIBLE): ListenableFuture { - var listenableFuture: ListenableFuture? = 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 { + var future: ListenableFuture? = 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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostModel.kt index 3ef5ea50fe..21c46796be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostModel.kt @@ -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 { 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)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostView.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostView.kt index 7ff3fea8de..fcff8d2f0a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryTextPostView.kt @@ -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 { - return linkPreviewView.bind(linkPreview, View.GONE) + fun bindLinkPreview(linkPreview: LinkPreview?, useLargeThumbnail: Boolean, loadThumbnail: Boolean = true): ListenableFuture { + 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() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostPreviewFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostPreviewFragment.kt index 5608aabba6..c9e2787bc4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostPreviewFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/text/StoryTextPostPreviewFragment.kt @@ -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 { diff --git a/app/src/main/res/layout/stories_text_post_link_preview.xml b/app/src/main/res/layout/stories_text_post_link_preview.xml index afd764699b..d360f7215a 100644 --- a/app/src/main/res/layout/stories_text_post_link_preview.xml +++ b/app/src/main/res/layout/stories_text_post_link_preview.xml @@ -4,16 +4,18 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + tools:background="@color/signal_dark_colorBackground" tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> - + android:layout_height="wrap_content"> - + + - @@ -82,22 +101,24 @@ android:layout_height="wrap_content" android:ellipsize="end" android:maxLines="1" - android:textAppearance="@style/TextAppearance.Signal.Body2" - android:textColor="@color/core_white" + android:textAppearance="@style/Signal.Text.BodyMedium" + android:textColor="@color/signal_light_colorOnSurfaceVariant" app:layout_constraintBottom_toTopOf="@id/link_preview_url" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@id/link_preview_title" app:layout_constraintTop_toBottomOf="@id/link_preview_title" + app:layout_goneMarginBottom="12dp" tools:text="Blah blah blah" /> - + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index d500ef4902..3e6f64f5e9 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -13,8 +13,7 @@ @drawable/contact_selection_checkbox - + - + - + - + - + - + - - - +