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");
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.
*/

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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