Allow generic links to be sent as stories.

This commit is contained in:
Alex Hart
2022-04-08 09:33:30 -03:00
committed by Cody Henthorne
parent 65835606cc
commit c4817ac017
8 changed files with 129 additions and 31 deletions

View File

@@ -49,6 +49,17 @@ public final class LinkPreviewUtil {
private static final Set<String> 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.
*/

View File

@@ -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<LinkPreview> 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> linkPreview;
private final LinkPreviewRepository.Error error;
private LinkPreviewState(boolean isLoading,
private LinkPreviewState(@Nullable String activeUrlForError,
boolean isLoading,
boolean hasLinks,
Optional<LinkPreview> 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() {

View File

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

View File

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

View File

@@ -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() {

View File

@@ -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<Boolean> {
var listenableFuture: ListenableFuture<Boolean>? = 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) {