mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 02:10:44 +01:00
Add stories link treatment for devices with link previews disabled.
This commit is contained in:
committed by
Greyson Parrelli
parent
552592db39
commit
9a21f5abca
@@ -13,6 +13,7 @@ import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.net.RequestController;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
@@ -34,12 +35,15 @@ public class LinkPreviewViewModel extends ViewModel {
|
||||
private Debouncer debouncer;
|
||||
private boolean enabled;
|
||||
|
||||
private LinkPreviewViewModel(@NonNull LinkPreviewRepository repository) {
|
||||
private final boolean enablePlaceholder;
|
||||
|
||||
private LinkPreviewViewModel(@NonNull LinkPreviewRepository repository, boolean enablePlaceholder) {
|
||||
this.repository = repository;
|
||||
this.enablePlaceholder = enablePlaceholder;
|
||||
this.linkPreviewState = new MutableLiveData<>();
|
||||
this.debouncer = new Debouncer(250);
|
||||
this.enabled = SignalStore.settings().isLinkPreviewsEnabled();
|
||||
this.linkPreviewSafeState = Transformations.map(linkPreviewState, state -> enabled ? state : LinkPreviewState.forNoLinks());
|
||||
this.linkPreviewSafeState = Transformations.map(linkPreviewState, state -> cleanseState(state));
|
||||
}
|
||||
|
||||
public LiveData<LinkPreviewState> getLinkPreviewState() {
|
||||
@@ -114,9 +118,8 @@ public class LinkPreviewViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
public void onTextChanged(@NonNull Context context, @NonNull String text, int cursorStart, int cursorEnd) {
|
||||
if (!enabled) return;
|
||||
if (!enabled && !enablePlaceholder) return;
|
||||
|
||||
final Context applicationContext = context.getApplicationContext();
|
||||
debouncer.publish(() -> {
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
userCanceled = false;
|
||||
@@ -147,35 +150,7 @@ public class LinkPreviewViewModel extends ViewModel {
|
||||
linkPreviewState.setValue(LinkPreviewState.forLoading());
|
||||
|
||||
activeUrl = link.get().getUrl();
|
||||
activeRequest = repository.getLinkPreview(applicationContext, link.get().getUrl(), new LinkPreviewRepository.Callback() {
|
||||
@Override
|
||||
public void onSuccess(@NonNull LinkPreview linkPreview) {
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
if (!userCanceled) {
|
||||
if (activeUrl != null && activeUrl.equals(linkPreview.getUrl())) {
|
||||
linkPreviewState.setValue(LinkPreviewState.forPreview(linkPreview));
|
||||
} else {
|
||||
linkPreviewState.setValue(LinkPreviewState.forNoLinks());
|
||||
}
|
||||
}
|
||||
activeRequest = null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull LinkPreviewRepository.Error error) {
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
if (!userCanceled) {
|
||||
if (activeUrl != null) {
|
||||
linkPreviewState.setValue(LinkPreviewState.forLinksWithNoPreview(activeUrl, error));
|
||||
} else {
|
||||
linkPreviewState.setValue(LinkPreviewState.forNoLinks());
|
||||
}
|
||||
}
|
||||
activeRequest = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
activeRequest = enabled ? performRequest(activeUrl) : createPlaceholder(activeUrl);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -226,9 +201,71 @@ public class LinkPreviewViewModel extends ViewModel {
|
||||
return cursorStart < link.getPosition() || cursorStart > link.getPosition() + link.getUrl().length();
|
||||
}
|
||||
|
||||
private @Nullable RequestController createPlaceholder(String url) {
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
if (!userCanceled) {
|
||||
if (activeUrl != null && activeUrl.equals(url)) {
|
||||
linkPreviewState.setValue(LinkPreviewState.forLinksWithNoPreview(url, LinkPreviewRepository.Error.PREVIEW_NOT_AVAILABLE));
|
||||
} else {
|
||||
linkPreviewState.setValue(LinkPreviewState.forNoLinks());
|
||||
}
|
||||
}
|
||||
|
||||
activeRequest = null;
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private @Nullable RequestController performRequest(String url) {
|
||||
return repository.getLinkPreview(ApplicationDependencies.getApplication(), url, new LinkPreviewRepository.Callback() {
|
||||
@Override
|
||||
public void onSuccess(@NonNull LinkPreview linkPreview) {
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
if (!userCanceled) {
|
||||
if (activeUrl != null && activeUrl.equals(linkPreview.getUrl())) {
|
||||
linkPreviewState.setValue(LinkPreviewState.forPreview(linkPreview));
|
||||
} else {
|
||||
linkPreviewState.setValue(LinkPreviewState.forNoLinks());
|
||||
}
|
||||
}
|
||||
activeRequest = null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull LinkPreviewRepository.Error error) {
|
||||
ThreadUtil.runOnMain(() -> {
|
||||
if (!userCanceled) {
|
||||
if (activeUrl != null) {
|
||||
linkPreviewState.setValue(LinkPreviewState.forLinksWithNoPreview(activeUrl, error));
|
||||
} else {
|
||||
linkPreviewState.setValue(LinkPreviewState.forNoLinks());
|
||||
}
|
||||
}
|
||||
activeRequest = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private @NonNull LinkPreviewState cleanseState(@NonNull LinkPreviewState state) {
|
||||
if (enabled) {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (enablePlaceholder) {
|
||||
return state.linkPreview
|
||||
.map(linkPreview -> LinkPreviewState.forLinksWithNoPreview(linkPreview.getUrl(), LinkPreviewRepository.Error.PREVIEW_NOT_AVAILABLE))
|
||||
.orElse(state);
|
||||
}
|
||||
|
||||
return LinkPreviewState.forNoLinks();
|
||||
}
|
||||
|
||||
public static class LinkPreviewState {
|
||||
private final String activeUrlForError;
|
||||
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;
|
||||
@@ -290,14 +327,20 @@ public class LinkPreviewViewModel extends ViewModel {
|
||||
public static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
|
||||
private final LinkPreviewRepository repository;
|
||||
private final boolean enablePlaceholder;
|
||||
|
||||
public Factory(@NonNull LinkPreviewRepository repository) {
|
||||
this.repository = repository;
|
||||
this(repository, false);
|
||||
}
|
||||
|
||||
public Factory(@NonNull LinkPreviewRepository repository, boolean enablePlaceholder) {
|
||||
this.repository = repository;
|
||||
this.enablePlaceholder = enablePlaceholder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return modelClass.cast(new LinkPreviewViewModel(repository));
|
||||
return modelClass.cast(new LinkPreviewViewModel(repository, enablePlaceholder));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creati
|
||||
requireActivity()
|
||||
},
|
||||
factoryProducer = {
|
||||
LinkPreviewViewModel.Factory(LinkPreviewRepository())
|
||||
LinkPreviewViewModel.Factory(LinkPreviewRepository(), true)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class TextStoryPostLinkEntryFragment : KeyboardEntryDialogFragment(
|
||||
private lateinit var input: EditText
|
||||
|
||||
private val linkPreviewViewModel: LinkPreviewViewModel by viewModels(
|
||||
factoryProducer = { LinkPreviewViewModel.Factory(LinkPreviewRepository()) }
|
||||
factoryProducer = { LinkPreviewViewModel.Factory(LinkPreviewRepository(), true) }
|
||||
)
|
||||
|
||||
private val viewModel: TextStoryPostCreationViewModel by viewModels(
|
||||
|
||||
@@ -2,13 +2,15 @@ package org.thoughtcrime.securesms.stories
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import org.signal.core.util.isAbsent
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView
|
||||
import org.thoughtcrime.securesms.databinding.StoriesTextPostLinkPreviewBinding
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
|
||||
@@ -32,18 +34,11 @@ class StoryLinkPreviewView @JvmOverloads constructor(
|
||||
inflate(context, R.layout.stories_text_post_link_preview, this)
|
||||
}
|
||||
|
||||
private val card: View = findViewById(R.id.link_preview_card)
|
||||
private val close: View = findViewById(R.id.link_preview_close)
|
||||
private val smallImage: ThumbnailView = findViewById<ThumbnailView>(R.id.link_preview_image).apply { isClickable = false }
|
||||
private val largeImage: ThumbnailView = findViewById<ThumbnailView>(R.id.link_preview_large).apply { isClickable = false }
|
||||
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)
|
||||
private val loadingSpinner: Stub<View> = Stub(findViewById(R.id.loading_spinner))
|
||||
private val binding = StoriesTextPostLinkPreviewBinding.bind(this)
|
||||
private val spinnerStub = Stub<View>(binding.loadingSpinner)
|
||||
|
||||
private fun getThumbnailTarget(useLargeThumbnail: Boolean): ThumbnailView {
|
||||
return if (useLargeThumbnail) largeImage else smallImage
|
||||
return if (useLargeThumbnail) binding.linkPreviewLarge else binding.linkPreviewImage
|
||||
}
|
||||
|
||||
fun getThumbnailViewWidth(useLargeThumbnail: Boolean): Int {
|
||||
@@ -62,37 +57,10 @@ class StoryLinkPreviewView @JvmOverloads constructor(
|
||||
fun bind(linkPreview: LinkPreview?, hiddenVisibility: Int = View.INVISIBLE, useLargeThumbnail: Boolean, loadThumbnail: Boolean = true): ListenableFuture<Boolean> {
|
||||
var future: ListenableFuture<Boolean>? = null
|
||||
|
||||
if (linkPreview != null) {
|
||||
visibility = View.VISIBLE
|
||||
|
||||
val image = getThumbnailTarget(useLargeThumbnail)
|
||||
val notImage = getThumbnailTarget(!useLargeThumbnail)
|
||||
|
||||
notImage.visible = false
|
||||
|
||||
val imageSlide: Slide? = linkPreview.thumbnail.map { ImageSlide(context, it) }.orElse(null)
|
||||
if (imageSlide != null) {
|
||||
if (loadThumbnail) {
|
||||
future = image.setImageResource(
|
||||
GlideApp.with(this),
|
||||
imageSlide,
|
||||
false,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
image.visible = true
|
||||
fallbackIcon.visible = false
|
||||
} else {
|
||||
image.visible = false
|
||||
fallbackIcon.visible = true
|
||||
}
|
||||
|
||||
title.text = linkPreview.title
|
||||
description.text = linkPreview.description
|
||||
description.visible = linkPreview.description.isNotEmpty()
|
||||
|
||||
formatUrl(linkPreview)
|
||||
if (isPartialLinkPreview(linkPreview)) {
|
||||
future = bindPartialLinkPreview(linkPreview!!)
|
||||
} else if (linkPreview != null) {
|
||||
future = bindFullLinkPreview(linkPreview, useLargeThumbnail, loadThumbnail)
|
||||
} else {
|
||||
visibility = hiddenVisibility
|
||||
}
|
||||
@@ -109,44 +77,100 @@ class StoryLinkPreviewView @JvmOverloads constructor(
|
||||
|
||||
bind(linkPreview, hiddenVisibility, useLargeThumbnail)
|
||||
|
||||
loadingSpinner.get().visible = linkPreviewState.isLoading
|
||||
spinnerStub.get().visible = linkPreviewState.isLoading
|
||||
if (linkPreviewState.isLoading) {
|
||||
visible = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPartialLinkPreview(linkPreview: LinkPreview?): Boolean {
|
||||
return linkPreview != null &&
|
||||
TextUtils.isEmpty(linkPreview.description) &&
|
||||
linkPreview.thumbnail.isAbsent() &&
|
||||
linkPreview.attachmentId == null
|
||||
}
|
||||
|
||||
private fun bindPartialLinkPreview(linkPreview: LinkPreview): ListenableFuture<Boolean> {
|
||||
visibility = View.VISIBLE
|
||||
binding.linkPreviewCard.visible = false
|
||||
binding.linkPreviewPlaceholderCard.visible = true
|
||||
|
||||
binding.linkPreviewPlaceholderTitle.text = Uri.parse(linkPreview.url).host
|
||||
|
||||
return SettableFuture(false)
|
||||
}
|
||||
|
||||
private fun bindFullLinkPreview(linkPreview: LinkPreview, useLargeThumbnail: Boolean, loadThumbnail: Boolean): ListenableFuture<Boolean> {
|
||||
visibility = View.VISIBLE
|
||||
binding.linkPreviewCard.visible = true
|
||||
binding.linkPreviewPlaceholderCard.visible = false
|
||||
|
||||
val image = getThumbnailTarget(useLargeThumbnail)
|
||||
val notImage = getThumbnailTarget(!useLargeThumbnail)
|
||||
var future: ListenableFuture<Boolean>? = null
|
||||
|
||||
notImage.visible = false
|
||||
|
||||
val imageSlide: Slide? = linkPreview.thumbnail.map { ImageSlide(context, it) }.orElse(null)
|
||||
if (imageSlide != null) {
|
||||
if (loadThumbnail) {
|
||||
future = image.setImageResource(
|
||||
GlideApp.with(this),
|
||||
imageSlide,
|
||||
false,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
image.visible = true
|
||||
binding.linkPreviewFallbackIcon.visible = false
|
||||
} else {
|
||||
image.visible = false
|
||||
binding.linkPreviewFallbackIcon.visible = true
|
||||
}
|
||||
|
||||
binding.linkPreviewTitle.text = linkPreview.title
|
||||
binding.linkPreviewDescription.text = linkPreview.description
|
||||
binding.linkPreviewDescription.visible = linkPreview.description.isNotEmpty()
|
||||
|
||||
formatUrl(linkPreview)
|
||||
|
||||
return future ?: SettableFuture(false)
|
||||
}
|
||||
|
||||
private fun formatUrl(linkPreview: LinkPreview) {
|
||||
val domain: String? = LinkPreviewUtil.getTopLevelDomain(linkPreview.url)
|
||||
|
||||
if (linkPreview.title == domain) {
|
||||
url.visibility = View.GONE
|
||||
binding.linkPreviewUrl.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
if (domain != null && linkPreview.date > 0) {
|
||||
url.text = url.context.getString(R.string.LinkPreviewView_domain_date, domain, formatDate(linkPreview.date))
|
||||
url.visibility = View.VISIBLE
|
||||
binding.linkPreviewUrl.text = context.getString(R.string.LinkPreviewView_domain_date, domain, formatDate(linkPreview.date))
|
||||
binding.linkPreviewUrl.visibility = View.VISIBLE
|
||||
} else if (domain != null) {
|
||||
url.text = domain
|
||||
url.visibility = View.VISIBLE
|
||||
binding.linkPreviewUrl.text = domain
|
||||
binding.linkPreviewUrl.visibility = View.VISIBLE
|
||||
} else if (linkPreview.date > 0) {
|
||||
url.text = formatDate(linkPreview.date)
|
||||
url.visibility = View.VISIBLE
|
||||
binding.linkPreviewUrl.text = formatDate(linkPreview.date)
|
||||
binding.linkPreviewUrl.visibility = View.VISIBLE
|
||||
} else {
|
||||
url.visibility = View.GONE
|
||||
binding.linkPreviewUrl.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun setOnCloseClickListener(onClickListener: OnClickListener?) {
|
||||
close.setOnClickListener(onClickListener)
|
||||
binding.linkPreviewClose.setOnClickListener(onClickListener)
|
||||
}
|
||||
|
||||
fun setOnPreviewClickListener(onClickListener: OnClickListener?) {
|
||||
card.setOnClickListener(onClickListener)
|
||||
binding.linkPreviewCard.setOnClickListener(onClickListener)
|
||||
binding.linkPreviewPlaceholderCard.setOnClickListener(onClickListener)
|
||||
}
|
||||
|
||||
fun setCanClose(canClose: Boolean) {
|
||||
close.visible = canClose
|
||||
binding.linkPreviewClose.visible = canClose
|
||||
}
|
||||
|
||||
private fun formatDate(date: Long): String? {
|
||||
|
||||
Reference in New Issue
Block a user