mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 00:59:49 +01:00
Add LinkPreview support to CFV2.
This commit is contained in:
committed by
Nicholas Tinsley
parent
3bdffed8c9
commit
2fbcc23451
@@ -12,6 +12,7 @@ import androidx.core.util.Consumer;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.signal.core.util.Hex;
|
||||
import org.signal.core.util.Result;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
@@ -62,6 +63,8 @@ import java.io.InputStream;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import okhttp3.CacheControl;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.OkHttpClient;
|
||||
@@ -86,6 +89,28 @@ public class LinkPreviewRepository {
|
||||
.build();
|
||||
}
|
||||
|
||||
public @NonNull Single<Result<LinkPreview, Error>> getLinkPreview(@NonNull String url) {
|
||||
return Single.<Result<LinkPreview, Error>>create(emitter -> {
|
||||
RequestController controller = getLinkPreview(ApplicationDependencies.getApplication(),
|
||||
url,
|
||||
new Callback() {
|
||||
@Override
|
||||
public void onSuccess(@NonNull LinkPreview linkPreview) {
|
||||
emitter.onSuccess(Result.success(linkPreview));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull Error error) {
|
||||
emitter.onSuccess(Result.failure(error));
|
||||
}
|
||||
});
|
||||
|
||||
if (controller != null) {
|
||||
emitter.setCancellable(controller::cancel);
|
||||
}
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
@Nullable RequestController getLinkPreview(@NonNull Context context,
|
||||
@NonNull String url,
|
||||
@NonNull Callback callback)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.linkpreview;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class LinkPreviewState {
|
||||
private final String activeUrlForError;
|
||||
private final boolean isLoading;
|
||||
private final boolean hasLinks;
|
||||
private final Optional<LinkPreview> linkPreview;
|
||||
private final LinkPreviewRepository.Error error;
|
||||
|
||||
private LinkPreviewState(@Nullable String activeUrlForError,
|
||||
boolean isLoading,
|
||||
boolean hasLinks,
|
||||
Optional<LinkPreview> linkPreview,
|
||||
@Nullable LinkPreviewRepository.Error error)
|
||||
{
|
||||
this.activeUrlForError = activeUrlForError;
|
||||
this.isLoading = isLoading;
|
||||
this.hasLinks = hasLinks;
|
||||
this.linkPreview = linkPreview;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public static LinkPreviewState forLoading() {
|
||||
return new LinkPreviewState(null, true, false, Optional.empty(), null);
|
||||
}
|
||||
|
||||
public static LinkPreviewState forPreview(@NonNull LinkPreview linkPreview) {
|
||||
return new LinkPreviewState(null, false, true, Optional.of(linkPreview), null);
|
||||
}
|
||||
|
||||
public static LinkPreviewState forLinksWithNoPreview(@Nullable String activeUrlForError, @NonNull LinkPreviewRepository.Error error) {
|
||||
return new LinkPreviewState(activeUrlForError, false, true, Optional.empty(), error);
|
||||
}
|
||||
|
||||
public static LinkPreviewState forNoLinks() {
|
||||
return new LinkPreviewState(null, false, false, Optional.empty(), null);
|
||||
}
|
||||
|
||||
public @Nullable String getActiveUrlForError() {
|
||||
return activeUrlForError;
|
||||
}
|
||||
|
||||
public boolean isLoading() {
|
||||
return isLoading;
|
||||
}
|
||||
|
||||
public boolean hasLinks() {
|
||||
return hasLinks;
|
||||
}
|
||||
|
||||
public Optional<LinkPreview> getLinkPreview() {
|
||||
return linkPreview;
|
||||
}
|
||||
|
||||
public @Nullable LinkPreviewRepository.Error getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public boolean hasContent() {
|
||||
return isLoading || hasLinks;
|
||||
}
|
||||
}
|
||||
@@ -255,7 +255,7 @@ public class LinkPreviewViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
if (enablePlaceholder) {
|
||||
return state.linkPreview
|
||||
return state.getLinkPreview()
|
||||
.map(linkPreview -> LinkPreviewState.forLinksWithNoPreview(linkPreview.getUrl(), LinkPreviewRepository.Error.PREVIEW_NOT_AVAILABLE))
|
||||
.orElse(state);
|
||||
}
|
||||
@@ -263,67 +263,6 @@ public class LinkPreviewViewModel extends ViewModel {
|
||||
return LinkPreviewState.forNoLinks();
|
||||
}
|
||||
|
||||
public static class LinkPreviewState {
|
||||
private final String activeUrlForError;
|
||||
private final boolean isLoading;
|
||||
private final boolean hasLinks;
|
||||
private final Optional<LinkPreview> linkPreview;
|
||||
private final LinkPreviewRepository.Error error;
|
||||
|
||||
private LinkPreviewState(@Nullable String activeUrlForError,
|
||||
boolean isLoading,
|
||||
boolean hasLinks,
|
||||
Optional<LinkPreview> linkPreview,
|
||||
@Nullable LinkPreviewRepository.Error error)
|
||||
{
|
||||
this.activeUrlForError = activeUrlForError;
|
||||
this.isLoading = isLoading;
|
||||
this.hasLinks = hasLinks;
|
||||
this.linkPreview = linkPreview;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
private static LinkPreviewState forLoading() {
|
||||
return new LinkPreviewState(null, true, false, Optional.empty(), null);
|
||||
}
|
||||
|
||||
private static LinkPreviewState forPreview(@NonNull LinkPreview linkPreview) {
|
||||
return new LinkPreviewState(null, false, true, Optional.of(linkPreview), null);
|
||||
}
|
||||
|
||||
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(null, false, false, Optional.empty(), null);
|
||||
}
|
||||
|
||||
public @Nullable String getActiveUrlForError() {
|
||||
return activeUrlForError;
|
||||
}
|
||||
|
||||
public boolean isLoading() {
|
||||
return isLoading;
|
||||
}
|
||||
|
||||
public boolean hasLinks() {
|
||||
return hasLinks;
|
||||
}
|
||||
|
||||
public Optional<LinkPreview> getLinkPreview() {
|
||||
return linkPreview;
|
||||
}
|
||||
|
||||
public @Nullable LinkPreviewRepository.Error getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
boolean hasContent() {
|
||||
return isLoading || hasLinks;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
|
||||
private final LinkPreviewRepository repository;
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.linkpreview
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.Result
|
||||
import org.signal.core.util.isAbsent
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.Debouncer
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* Rewrite of [LinkPreviewViewModel] preferring Rx and Kotlin
|
||||
*/
|
||||
class LinkPreviewViewModelV2(
|
||||
private val linkPreviewRepository: LinkPreviewRepository = LinkPreviewRepository(),
|
||||
private val enablePlaceholder: Boolean
|
||||
) : ViewModel() {
|
||||
private var enabled = SignalStore.settings().isLinkPreviewsEnabled
|
||||
private val linkPreviewStateStore = RxStore<LinkPreviewState>(LinkPreviewState.forNoLinks())
|
||||
|
||||
val linkPreviewState: Flowable<LinkPreviewState> = linkPreviewStateStore.stateFlowable
|
||||
val hasLinkPreview: Boolean = linkPreviewStateStore.state.linkPreview.isPresent
|
||||
val hasLinkPreviewUi: Boolean = linkPreviewStateStore.state.hasContent()
|
||||
|
||||
private var activeUrl: String? = null
|
||||
private var activeRequest: Disposable = Disposable.disposed()
|
||||
private var userCancelled: Boolean = false
|
||||
private val debouncer: Debouncer = Debouncer(250)
|
||||
|
||||
override fun onCleared() {
|
||||
activeRequest.dispose()
|
||||
debouncer.clear()
|
||||
}
|
||||
|
||||
fun onSend(): List<LinkPreview> {
|
||||
val currentState = linkPreviewStateStore.state
|
||||
|
||||
onUserCancel()
|
||||
|
||||
return currentState.linkPreview.map { listOf(it) }.orElse(emptyList())
|
||||
}
|
||||
|
||||
fun onTextChanged(text: String, cursorStart: Int, cursorEnd: Int) {
|
||||
if (!enabled && !enablePlaceholder) {
|
||||
return
|
||||
}
|
||||
|
||||
debouncer.publish {
|
||||
if (text.isEmpty()) {
|
||||
userCancelled = false
|
||||
}
|
||||
|
||||
if (userCancelled) {
|
||||
return@publish
|
||||
}
|
||||
|
||||
val link: Optional<Link> = LinkPreviewUtil.findValidPreviewUrls(text).findFirst()
|
||||
|
||||
activeRequest.dispose()
|
||||
|
||||
if (link.isAbsent() || !isCursorPositionValid(text, link.get(), cursorStart, cursorEnd)) {
|
||||
activeUrl = null
|
||||
setLinkPreviewState(LinkPreviewState.forNoLinks())
|
||||
return@publish
|
||||
}
|
||||
|
||||
setLinkPreviewState(LinkPreviewState.forLoading())
|
||||
|
||||
val activeUrl = link.get().url
|
||||
this.activeUrl = activeUrl
|
||||
activeRequest = if (enabled) {
|
||||
performRequest(activeUrl)
|
||||
} else {
|
||||
createPlaceholder(activeUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onEnabled() {
|
||||
userCancelled = false
|
||||
enabled = SignalStore.settings().isLinkPreviewsEnabled
|
||||
}
|
||||
|
||||
fun onUserCancel() {
|
||||
activeRequest.dispose()
|
||||
userCancelled = true
|
||||
activeUrl = null
|
||||
debouncer.clear()
|
||||
setLinkPreviewState(LinkPreviewState.forNoLinks())
|
||||
}
|
||||
|
||||
private fun isCursorPositionValid(text: String, link: Link, cursorStart: Int, cursorEnd: Int): Boolean {
|
||||
if (cursorStart != cursorEnd) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (text.endsWith(link.url) && cursorStart == link.position + link.url.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
return cursorStart < link.position || cursorStart > link.position + link.url.length
|
||||
}
|
||||
|
||||
private fun createPlaceholder(url: String): Disposable {
|
||||
return Single.just(url)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy {
|
||||
if (!userCancelled) {
|
||||
if (activeUrl != null && activeUrl == url) {
|
||||
setLinkPreviewState(LinkPreviewState.forLinksWithNoPreview(url, LinkPreviewRepository.Error.PREVIEW_NOT_AVAILABLE))
|
||||
} else {
|
||||
setLinkPreviewState(LinkPreviewState.forNoLinks())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun performRequest(url: String): Disposable {
|
||||
return linkPreviewRepository.getLinkPreview(url)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy { result ->
|
||||
if (!userCancelled) {
|
||||
val linkPreviewState = when (result) {
|
||||
is Result.Success -> if (activeUrl == result.success.url) LinkPreviewState.forPreview(result.success) else LinkPreviewState.forNoLinks()
|
||||
is Result.Failure -> if (activeUrl != null) LinkPreviewState.forLinksWithNoPreview(activeUrl, result.failure) else LinkPreviewState.forNoLinks()
|
||||
}
|
||||
|
||||
setLinkPreviewState(linkPreviewState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setLinkPreviewState(linkPreviewState: LinkPreviewState) {
|
||||
linkPreviewStateStore.update { cleanseState(linkPreviewState) }
|
||||
}
|
||||
|
||||
private fun cleanseState(linkPreviewState: LinkPreviewState): LinkPreviewState {
|
||||
if (enabled) {
|
||||
return linkPreviewState
|
||||
}
|
||||
|
||||
if (enablePlaceholder) {
|
||||
return linkPreviewState
|
||||
.linkPreview
|
||||
.map { LinkPreviewState.forLinksWithNoPreview(it.url, LinkPreviewRepository.Error.PREVIEW_NOT_AVAILABLE) }
|
||||
.orElse(linkPreviewState)
|
||||
}
|
||||
|
||||
return LinkPreviewState.forNoLinks()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user