Allow generic links to be sent as stories.

fork-5.53.8
Alex Hart 2022-04-08 09:33:30 -03:00 zatwierdzone przez Cody Henthorne
rodzic 65835606cc
commit c4817ac017
8 zmienionych plików z 129 dodań i 31 usunięć

Wyświetl plik

@ -49,6 +49,17 @@ public final class LinkPreviewUtil {
private static final Set<String> INVALID_TOP_LEVEL_DOMAINS = SetUtil.newHashSet("onion", "i2p"); 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. * @return All whitelisted URLs in the source text.
*/ */

Wyświetl plik

@ -12,6 +12,7 @@ import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import org.signal.core.util.ThreadUtil; import org.signal.core.util.ThreadUtil;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.net.RequestController; import org.thoughtcrime.securesms.net.RequestController;
import org.thoughtcrime.securesms.util.Debouncer; 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) { public void onTextChanged(@NonNull Context context, @NonNull String text, int cursorStart, int cursorEnd) {
if (!enabled) return; if (!enabled) return;
@ -131,7 +167,7 @@ public class LinkPreviewViewModel extends ViewModel {
ThreadUtil.runOnMain(() -> { ThreadUtil.runOnMain(() -> {
if (!userCanceled) { if (!userCanceled) {
if (activeUrl != null) { if (activeUrl != null) {
linkPreviewState.setValue(LinkPreviewState.forLinksWithNoPreview(error)); linkPreviewState.setValue(LinkPreviewState.forLinksWithNoPreview(activeUrl, error));
} else { } else {
linkPreviewState.setValue(LinkPreviewState.forNoLinks()); linkPreviewState.setValue(LinkPreviewState.forNoLinks());
} }
@ -191,36 +227,43 @@ public class LinkPreviewViewModel extends ViewModel {
} }
public static class LinkPreviewState { public static class LinkPreviewState {
private final boolean isLoading; private final String activeUrlForError;
private final boolean isLoading;
private final boolean hasLinks; private final boolean hasLinks;
private final Optional<LinkPreview> linkPreview; private final Optional<LinkPreview> linkPreview;
private final LinkPreviewRepository.Error error; private final LinkPreviewRepository.Error error;
private LinkPreviewState(boolean isLoading, private LinkPreviewState(@Nullable String activeUrlForError,
boolean isLoading,
boolean hasLinks, boolean hasLinks,
Optional<LinkPreview> linkPreview, Optional<LinkPreview> linkPreview,
@Nullable LinkPreviewRepository.Error error) @Nullable LinkPreviewRepository.Error error)
{ {
this.isLoading = isLoading; this.activeUrlForError = activeUrlForError;
this.hasLinks = hasLinks; this.isLoading = isLoading;
this.linkPreview = linkPreview; this.hasLinks = hasLinks;
this.error = error; this.linkPreview = linkPreview;
this.error = error;
} }
private static LinkPreviewState forLoading() { 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) { 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) { private static LinkPreviewState forLinksWithNoPreview(@Nullable String activeUrlForError, @NonNull LinkPreviewRepository.Error error) {
return new LinkPreviewState(false, true, Optional.empty(), error); return new LinkPreviewState(activeUrlForError, false, true, Optional.empty(), error);
} }
private static LinkPreviewState forNoLinks() { 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() { public boolean isLoading() {

Wyświetl plik

@ -136,7 +136,7 @@ class TextStoryPostCreationViewModel : ViewModel() {
store.update { it.copy(backgroundColor = TextStoryBackgroundColors.cycleBackgroundColor(it.backgroundColor)) } store.update { it.copy(backgroundColor = TextStoryBackgroundColors.cycleBackgroundColor(it.backgroundColor)) }
} }
fun setLinkPreview(url: String) { fun setLinkPreview(url: String?) {
store.update { it.copy(linkPreviewUri = url) } store.update { it.copy(linkPreviewUri = url) }
} }

Wyświetl plik

@ -55,8 +55,10 @@ class TextStoryPostLinkEntryFragment : KeyboardEntryDialogFragment(
) )
confirmButton.setOnClickListener { confirmButton.setOnClickListener {
if (linkPreviewViewModel.hasLinkPreview()) { val linkPreviewState = linkPreviewViewModel.linkPreviewState.value
viewModel.setLinkPreview(linkPreviewViewModel.linkPreviewState.value!!.linkPreview.get().url) if (linkPreviewState != null) {
val url = linkPreviewState.linkPreview.map { it.url }.orElseGet { linkPreviewState.activeUrlForError }
viewModel.setLinkPreview(url)
} }
dismissAllowingStateLoss() dismissAllowingStateLoss()
@ -64,8 +66,8 @@ class TextStoryPostLinkEntryFragment : KeyboardEntryDialogFragment(
linkPreviewViewModel.linkPreviewState.observe(viewLifecycleOwner) { state -> linkPreviewViewModel.linkPreviewState.observe(viewLifecycleOwner) { state ->
linkPreview.bind(state) linkPreview.bind(state)
shareALinkGroup.visible = !state.isLoading && !state.linkPreview.isPresent && state.error == null shareALinkGroup.visible = !state.isLoading && !state.linkPreview.isPresent && (state.error == null && state.activeUrlForError == null)
confirmButton.isEnabled = state.linkPreview.isPresent confirmButton.isEnabled = state.linkPreview.isPresent || state.activeUrlForError != null
progress.visible = state.isLoading progress.visible = state.isLoading
} }
} }

Wyświetl plik

@ -160,7 +160,11 @@ class TextStoryPostSendFragment : Fragment(R.layout.stories_send_text_post_fragm
val textStoryPostCreationState = creationViewModel.state.value val textStoryPostCreationState = creationViewModel.state.value
viewModel.onSend(contactSearchMediator.getSelectedContacts(), textStoryPostCreationState!!, linkPreviewViewModel.onSend().firstOrNull()) viewModel.onSend(
contactSearchMediator.getSelectedContacts(),
textStoryPostCreationState!!,
linkPreviewViewModel.onSendWithErrorUrl().firstOrNull()
)
} }
private fun animateInSelection() { private fun animateInSelection() {

Wyświetl plik

@ -3,17 +3,17 @@ package org.thoughtcrime.securesms.stories
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import okhttp3.HttpUrl
import org.signal.core.util.DimensionUnit import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.OutlinedThumbnailView import org.thoughtcrime.securesms.components.OutlinedThumbnailView
import org.thoughtcrime.securesms.linkpreview.LinkPreview import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.ImageSlide 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.ListenableFuture
import org.thoughtcrime.securesms.util.concurrent.SettableFuture import org.thoughtcrime.securesms.util.concurrent.SettableFuture
import org.thoughtcrime.securesms.util.visible 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 title: TextView = findViewById(R.id.link_preview_title)
private val url: TextView = findViewById(R.id.link_preview_url) private val url: TextView = findViewById(R.id.link_preview_url)
private val description: TextView = findViewById(R.id.link_preview_description) 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> { fun bind(linkPreview: LinkPreview?, hiddenVisibility: Int = View.INVISIBLE): ListenableFuture<Boolean> {
var listenableFuture: ListenableFuture<Boolean>? = null var listenableFuture: ListenableFuture<Boolean>? = null
@ -50,8 +51,10 @@ class StoryLinkPreviewView @JvmOverloads constructor(
if (imageSlide != null) { if (imageSlide != null) {
listenableFuture = image.setImageResource(GlideApp.with(image), imageSlide, false, false) listenableFuture = image.setImageResource(GlideApp.with(image), imageSlide, false, false)
image.visible = true image.visible = true
fallbackIcon.visible = false
} else { } else {
image.visible = false image.visible = false
fallbackIcon.visible = true
} }
title.text = linkPreview.title title.text = linkPreview.title
@ -68,17 +71,21 @@ class StoryLinkPreviewView @JvmOverloads constructor(
} }
fun bind(linkPreviewState: LinkPreviewViewModel.LinkPreviewState, hiddenVisibility: Int = View.INVISIBLE) { 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) { private fun formatUrl(linkPreview: LinkPreview) {
var domain: String? = null val domain: String? = LinkPreviewUtil.getTopLevelDomain(linkPreview.url)
if (!Util.isEmpty(linkPreview.url)) { if (linkPreview.title == domain) {
val url = HttpUrl.parse(linkPreview.url) url.visibility = View.GONE
if (url != null) { return
domain = url.topPrivateDomain()
}
} }
if (domain != null && linkPreview.date > 0) { if (domain != null && linkPreview.date > 0) {

Wyświetl plik

@ -28,6 +28,20 @@
android:paddingTop="12dp" android:paddingTop="12dp"
android:paddingBottom="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 <org.thoughtcrime.securesms.components.OutlinedThumbnailView
android:id="@+id/link_preview_image" android:id="@+id/link_preview_image"
android:layout_width="76dp" android:layout_width="76dp"
@ -36,21 +50,28 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/test_gradient" tools:src="@drawable/test_gradient" />
tools:visibility="visible" />
<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 <org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/link_preview_title" android:id="@+id/link_preview_title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="3" android:maxLines="3"
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold" android:textAppearance="@style/TextAppearance.Signal.Body1.Bold"
android:textColor="@color/core_white" android:textColor="@color/core_white"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/link_preview_description"
app:layout_constraintEnd_toEndOf="parent" 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_constraintTop_toTopOf="parent"
app:layout_goneMarginStart="0dp" 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." /> 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:maxLines="1"
android:textAppearance="@style/TextAppearance.Signal.Body2" android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/core_white" android:textColor="@color/core_white"
app:layout_constraintBottom_toTopOf="@id/link_preview_url"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/link_preview_title" app:layout_constraintStart_toStartOf="@id/link_preview_title"
app:layout_constraintTop_toBottomOf="@id/link_preview_title" app:layout_constraintTop_toBottomOf="@id/link_preview_title"
@ -76,6 +98,7 @@
android:maxLines="1" android:maxLines="1"
android:textAppearance="@style/TextAppearance.Signal.Body2" android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/transparent_white_60" android:textColor="@color/transparent_white_60"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/link_preview_title" app:layout_constraintStart_toStartOf="@id/link_preview_title"
app:layout_constraintTop_toBottomOf="@id/link_preview_description" app:layout_constraintTop_toBottomOf="@id/link_preview_description"

Wyświetl plik

@ -457,6 +457,14 @@
<item name="cornerSizeBottomRight">0dp</item> <item name="cornerSizeBottomRight">0dp</item>
</style> </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=""> <style name="ShapeAppearanceOverlay.Signal.Story.Preview" parent="">
<item name="cornerFamily">rounded</item> <item name="cornerFamily">rounded</item>
<item name="cornerSizeTopRight">12dp</item> <item name="cornerSizeTopRight">12dp</item>