diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java index 2e10459335..eed1d3ca73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java @@ -49,6 +49,17 @@ public final class LinkPreviewUtil { private static final Set INVALID_TOP_LEVEL_DOMAINS = SetUtil.newHashSet("onion", "i2p"); + public static @Nullable String getTopLevelDomain(@Nullable String urlString) { + if (!Util.isEmpty(urlString)) { + HttpUrl url = HttpUrl.parse(urlString); + if (url != null) { + return url.topPrivateDomain(); + } + } + + return null; + } + /** * @return All whitelisted URLs in the source text. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java index 42ca23f6d4..afc774988f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java @@ -12,6 +12,7 @@ import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import org.signal.core.util.ThreadUtil; +import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.net.RequestController; import org.thoughtcrime.securesms.util.Debouncer; @@ -77,6 +78,41 @@ public class LinkPreviewViewModel extends ViewModel { } } + /** + * Gets the current state for use in the UI, then resets local state to prepare for the next message send. + */ + public @NonNull List onSendWithErrorUrl() { + final LinkPreviewState currentState = linkPreviewSafeState.getValue(); + + if (activeRequest != null) { + activeRequest.cancel(); + activeRequest = null; + } + + userCanceled = false; + activeUrl = null; + + debouncer.clear(); + linkPreviewState.setValue(LinkPreviewState.forNoLinks()); + + if (currentState == null) { + return Collections.emptyList(); + } else if (currentState.getLinkPreview().isPresent()) { + return Collections.singletonList(currentState.getLinkPreview().get()); + } else if (currentState.getActiveUrlForError() != null) { + String topLevelDomain = LinkPreviewUtil.getTopLevelDomain(currentState.getActiveUrlForError()); + AttachmentId attachmentId = null; + + return Collections.singletonList(new LinkPreview(currentState.getActiveUrlForError(), + topLevelDomain != null ? topLevelDomain : currentState.getActiveUrlForError(), + null, + -1L, + attachmentId)); + } else { + return Collections.emptyList(); + } + } + public void onTextChanged(@NonNull Context context, @NonNull String text, int cursorStart, int cursorEnd) { if (!enabled) return; @@ -131,7 +167,7 @@ public class LinkPreviewViewModel extends ViewModel { ThreadUtil.runOnMain(() -> { if (!userCanceled) { if (activeUrl != null) { - linkPreviewState.setValue(LinkPreviewState.forLinksWithNoPreview(error)); + linkPreviewState.setValue(LinkPreviewState.forLinksWithNoPreview(activeUrl, error)); } else { linkPreviewState.setValue(LinkPreviewState.forNoLinks()); } @@ -191,36 +227,43 @@ public class LinkPreviewViewModel extends ViewModel { } public static class LinkPreviewState { - private final boolean isLoading; + private final String activeUrlForError; + private final boolean isLoading; private final boolean hasLinks; private final Optional linkPreview; private final LinkPreviewRepository.Error error; - private LinkPreviewState(boolean isLoading, + private LinkPreviewState(@Nullable String activeUrlForError, + boolean isLoading, boolean hasLinks, Optional linkPreview, @Nullable LinkPreviewRepository.Error error) { - this.isLoading = isLoading; - this.hasLinks = hasLinks; - this.linkPreview = linkPreview; - this.error = error; + this.activeUrlForError = activeUrlForError; + this.isLoading = isLoading; + this.hasLinks = hasLinks; + this.linkPreview = linkPreview; + this.error = error; } private static LinkPreviewState forLoading() { - return new LinkPreviewState(true, false, Optional.empty(), null); + return new LinkPreviewState(null, true, false, Optional.empty(), null); } private static LinkPreviewState forPreview(@NonNull LinkPreview linkPreview) { - return new LinkPreviewState(false, true, Optional.of(linkPreview), null); + return new LinkPreviewState(null, false, true, Optional.of(linkPreview), null); } - private static LinkPreviewState forLinksWithNoPreview(@NonNull LinkPreviewRepository.Error error) { - return new LinkPreviewState(false, true, Optional.empty(), error); + private static LinkPreviewState forLinksWithNoPreview(@Nullable String activeUrlForError, @NonNull LinkPreviewRepository.Error error) { + return new LinkPreviewState(activeUrlForError, false, true, Optional.empty(), error); } private static LinkPreviewState forNoLinks() { - return new LinkPreviewState(false, false, Optional.empty(), null); + return new LinkPreviewState(null, false, false, Optional.empty(), null); + } + + public @Nullable String getActiveUrlForError() { + return activeUrlForError; } public boolean isLoading() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationViewModel.kt index de4af0ce38..8050959631 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationViewModel.kt @@ -136,7 +136,7 @@ class TextStoryPostCreationViewModel : ViewModel() { store.update { it.copy(backgroundColor = TextStoryBackgroundColors.cycleBackgroundColor(it.backgroundColor)) } } - fun setLinkPreview(url: String) { + fun setLinkPreview(url: String?) { store.update { it.copy(linkPreviewUri = url) } } 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 fb6e3b0c6e..093033e980 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 @@ -55,8 +55,10 @@ class TextStoryPostLinkEntryFragment : KeyboardEntryDialogFragment( ) confirmButton.setOnClickListener { - if (linkPreviewViewModel.hasLinkPreview()) { - viewModel.setLinkPreview(linkPreviewViewModel.linkPreviewState.value!!.linkPreview.get().url) + val linkPreviewState = linkPreviewViewModel.linkPreviewState.value + if (linkPreviewState != null) { + val url = linkPreviewState.linkPreview.map { it.url }.orElseGet { linkPreviewState.activeUrlForError } + viewModel.setLinkPreview(url) } dismissAllowingStateLoss() @@ -64,8 +66,8 @@ class TextStoryPostLinkEntryFragment : KeyboardEntryDialogFragment( linkPreviewViewModel.linkPreviewState.observe(viewLifecycleOwner) { state -> linkPreview.bind(state) - shareALinkGroup.visible = !state.isLoading && !state.linkPreview.isPresent && state.error == null - confirmButton.isEnabled = state.linkPreview.isPresent + shareALinkGroup.visible = !state.isLoading && !state.linkPreview.isPresent && (state.error == null && state.activeUrlForError == null) + confirmButton.isEnabled = state.linkPreview.isPresent || state.activeUrlForError != null progress.visible = state.isLoading } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendFragment.kt index 14177e5d90..d53cb25359 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendFragment.kt @@ -160,7 +160,11 @@ class TextStoryPostSendFragment : Fragment(R.layout.stories_send_text_post_fragm val textStoryPostCreationState = creationViewModel.state.value - viewModel.onSend(contactSearchMediator.getSelectedContacts(), textStoryPostCreationState!!, linkPreviewViewModel.onSend().firstOrNull()) + viewModel.onSend( + contactSearchMediator.getSelectedContacts(), + textStoryPostCreationState!!, + linkPreviewViewModel.onSendWithErrorUrl().firstOrNull() + ) } private fun animateInSelection() { 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 1fb2f3a1f2..374d803c89 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/StoryLinkPreviewView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/StoryLinkPreviewView.kt @@ -3,17 +3,17 @@ package org.thoughtcrime.securesms.stories import android.content.Context import android.util.AttributeSet import android.view.View +import android.widget.ImageView import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout -import okhttp3.HttpUrl import org.signal.core.util.DimensionUnit import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.OutlinedThumbnailView 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.util.Util import org.thoughtcrime.securesms.util.concurrent.ListenableFuture import org.thoughtcrime.securesms.util.concurrent.SettableFuture import org.thoughtcrime.securesms.util.visible @@ -35,6 +35,7 @@ class StoryLinkPreviewView @JvmOverloads constructor( 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) fun bind(linkPreview: LinkPreview?, hiddenVisibility: Int = View.INVISIBLE): ListenableFuture { var listenableFuture: ListenableFuture? = null @@ -50,8 +51,10 @@ class StoryLinkPreviewView @JvmOverloads constructor( if (imageSlide != null) { listenableFuture = image.setImageResource(GlideApp.with(image), imageSlide, false, false) image.visible = true + fallbackIcon.visible = false } else { image.visible = false + fallbackIcon.visible = true } title.text = linkPreview.title @@ -68,17 +71,21 @@ class StoryLinkPreviewView @JvmOverloads constructor( } fun bind(linkPreviewState: LinkPreviewViewModel.LinkPreviewState, hiddenVisibility: Int = View.INVISIBLE) { - bind(linkPreviewState.linkPreview.orElse(null), hiddenVisibility) + val linkPreview: LinkPreview? = linkPreviewState.linkPreview.orElseGet { + linkPreviewState.activeUrlForError?.let { + LinkPreview(it, LinkPreviewUtil.getTopLevelDomain(it) ?: it, null, -1L, null) + } + } + + bind(linkPreview, hiddenVisibility) } private fun formatUrl(linkPreview: LinkPreview) { - var domain: String? = null + val domain: String? = LinkPreviewUtil.getTopLevelDomain(linkPreview.url) - if (!Util.isEmpty(linkPreview.url)) { - val url = HttpUrl.parse(linkPreview.url) - if (url != null) { - domain = url.topPrivateDomain() - } + if (linkPreview.title == domain) { + url.visibility = View.GONE + return } if (domain != null && linkPreview.date > 0) { 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 053a4ed5fc..80f3323226 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 @@ -28,6 +28,20 @@ android:paddingTop="12dp" android:paddingBottom="12dp"> + + + tools:src="@drawable/test_gradient" /> + + @@ -63,6 +84,7 @@ android:maxLines="1" android:textAppearance="@style/TextAppearance.Signal.Body2" android:textColor="@color/core_white" + 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" @@ -76,6 +98,7 @@ android:maxLines="1" android:textAppearance="@style/TextAppearance.Signal.Body2" android:textColor="@color/transparent_white_60" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@id/link_preview_title" app:layout_constraintTop_toBottomOf="@id/link_preview_description" diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 979da483aa..36ed7e7ab9 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -457,6 +457,14 @@ 0dp + +