Allow generic links to be sent as stories.
This commit is contained in:
parent
65835606cc
commit
c4817ac017
8 changed files with 129 additions and 31 deletions
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -28,6 +28,20 @@
|
|||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/link_preview_fallback_icon"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:background="@color/core_grey_02"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="centerInside"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearance="@style/ShapeAppearanceOverlay.Signal.Story.LinkPreview.Icon"
|
||||
app:srcCompat="@drawable/ic_link_24"
|
||||
app:tint="@color/core_grey_75" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.OutlinedThumbnailView
|
||||
android:id="@+id/link_preview_image"
|
||||
android:layout_width="76dp"
|
||||
|
@ -36,21 +50,28 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/test_gradient"
|
||||
tools:visibility="visible" />
|
||||
tools:src="@drawable/test_gradient" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/image_barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="end"
|
||||
app:barrierMargin="12dp"
|
||||
app:constraint_referenced_ids="link_preview_fallback_icon,link_preview_image" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/link_preview_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="3"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold"
|
||||
android:textColor="@color/core_white"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toTopOf="@id/link_preview_description"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/link_preview_image"
|
||||
app:layout_constraintStart_toEndOf="@id/image_barrier"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_goneMarginStart="0dp"
|
||||
tools:text="ASDF dot com, the resource of your asdf dreams and whatnot. This needs to be 3 lines for testing." />
|
||||
|
@ -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"
|
||||
|
|
|
@ -457,6 +457,14 @@
|
|||
<item name="cornerSizeBottomRight">0dp</item>
|
||||
</style>
|
||||
|
||||
<style name="ShapeAppearanceOverlay.Signal.Story.LinkPreview.Icon" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSizeTopRight">8dp</item>
|
||||
<item name="cornerSizeTopLeft">8dp</item>
|
||||
<item name="cornerSizeBottomLeft">8dp</item>
|
||||
<item name="cornerSizeBottomRight">8dp</item>
|
||||
</style>
|
||||
|
||||
<style name="ShapeAppearanceOverlay.Signal.Story.Preview" parent="">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSizeTopRight">12dp</item>
|
||||
|
|
Loading…
Add table
Reference in a new issue