Add SavedStateHandle support to LinkPreviewViewModelV2.

This commit is contained in:
Alex Hart
2023-09-29 08:25:17 -04:00
committed by GitHub
parent f18a03ee6d
commit d46daed49a
21 changed files with 500 additions and 143 deletions

View File

@@ -1,7 +1,11 @@
package org.thoughtcrime.securesms.linkpreview;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.os.ParcelCompat;
import androidx.core.text.HtmlCompat;
import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -15,7 +19,7 @@ import org.thoughtcrime.securesms.util.JsonUtils;
import java.io.IOException;
import java.util.Optional;
public class LinkPreview {
public class LinkPreview implements Parcelable {
@JsonProperty
private final String url;
@@ -67,6 +71,42 @@ public class LinkPreview {
this.thumbnail = Optional.empty();
}
protected LinkPreview(Parcel in) {
url = in.readString();
title = in.readString();
description = in.readString();
date = in.readLong();
attachmentId = ParcelCompat.readParcelable(in, AttachmentId.class.getClassLoader(), AttachmentId.class);
thumbnail = Optional.ofNullable(ParcelCompat.readParcelable(in, Attachment.class.getClassLoader(), Attachment.class));
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(url);
dest.writeString(title);
dest.writeString(description);
dest.writeLong(date);
dest.writeParcelable(attachmentId, flags);
dest.writeParcelable(thumbnail.orElse(null), 0);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<LinkPreview> CREATOR = new Creator<LinkPreview>() {
@Override
public LinkPreview createFromParcel(Parcel in) {
return new LinkPreview(in);
}
@Override
public LinkPreview[] newArray(int size) {
return new LinkPreview[size];
}
};
public @NonNull String getUrl() {
return url;
}

View File

@@ -1,72 +0,0 @@
/*
* 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;
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.linkpreview
import android.os.Parcelable
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import java.util.Optional
@Parcelize
class LinkPreviewState private constructor(
@JvmField val activeUrlForError: String?,
@JvmField val isLoading: Boolean,
private val hasLinks: Boolean,
private val preview: LinkPreview?,
@JvmField val error: LinkPreviewRepository.Error?
) : Parcelable {
@IgnoredOnParcel
@JvmField
val linkPreview: Optional<LinkPreview> = Optional.ofNullable(preview)
fun hasLinks(): Boolean {
return hasLinks
}
fun hasContent(): Boolean {
return isLoading || hasLinks
}
companion object {
@JvmStatic
fun forLoading(): LinkPreviewState {
return LinkPreviewState(
activeUrlForError = null,
isLoading = true,
hasLinks = false,
preview = null,
error = null
)
}
@JvmStatic
fun forPreview(linkPreview: LinkPreview): LinkPreviewState {
return LinkPreviewState(
activeUrlForError = null,
isLoading = false,
hasLinks = true,
preview = linkPreview,
error = null
)
}
@JvmStatic
fun forLinksWithNoPreview(activeUrlForError: String?, error: LinkPreviewRepository.Error): LinkPreviewState {
return LinkPreviewState(
activeUrlForError = activeUrlForError,
isLoading = false,
hasLinks = true,
preview = null,
error = error
)
}
@JvmStatic
fun forNoLinks(): LinkPreviewState {
return LinkPreviewState(
activeUrlForError = null,
isLoading = false,
hasLinks = false,
preview = null,
error = null
)
}
}
}

View File

@@ -50,14 +50,6 @@ public class LinkPreviewViewModel extends ViewModel {
return linkPreviewSafeState;
}
public boolean hasLinkPreview() {
return linkPreviewSafeState.getValue() != null && linkPreviewSafeState.getValue().getLinkPreview().isPresent();
}
public boolean hasLinkPreviewUi() {
return linkPreviewSafeState.getValue() != null && linkPreviewSafeState.getValue().hasContent();
}
/**
* Gets the current state for use in the UI, then resets local state to prepare for the next message send.
*/
@@ -75,10 +67,10 @@ public class LinkPreviewViewModel extends ViewModel {
debouncer.clear();
linkPreviewState.setValue(LinkPreviewState.forNoLinks());
if (currentState == null || !currentState.getLinkPreview().isPresent()) {
if (currentState == null || !currentState.linkPreview.isPresent()) {
return Collections.emptyList();
} else {
return Collections.singletonList(currentState.getLinkPreview().get());
return Collections.singletonList(currentState.linkPreview.get());
}
}
@@ -101,14 +93,14 @@ public class LinkPreviewViewModel extends ViewModel {
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());
} else if (currentState.linkPreview.isPresent()) {
return Collections.singletonList(currentState.linkPreview.get());
} else if (currentState.activeUrlForError != null) {
String topLevelDomain = LinkPreviewUtil.getTopLevelDomain(currentState.activeUrlForError);
AttachmentId attachmentId = null;
return Collections.singletonList(new LinkPreview(currentState.getActiveUrlForError(),
topLevelDomain != null ? topLevelDomain : currentState.getActiveUrlForError(),
return Collections.singletonList(new LinkPreview(currentState.activeUrlForError,
topLevelDomain != null ? topLevelDomain : currentState.activeUrlForError,
null,
-1L,
attachmentId));
@@ -255,7 +247,7 @@ public class LinkPreviewViewModel extends ViewModel {
}
if (enablePlaceholder) {
return state.getLinkPreview()
return state.linkPreview
.map(linkPreview -> LinkPreviewState.forLinksWithNoPreview(linkPreview.getUrl(), LinkPreviewRepository.Error.PREVIEW_NOT_AVAILABLE))
.orElse(state);
}

View File

@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.linkpreview
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
@@ -22,23 +23,50 @@ import java.util.Optional
* Rewrite of [LinkPreviewViewModel] preferring Rx and Kotlin
*/
class LinkPreviewViewModelV2(
private val savedStateHandle: SavedStateHandle,
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
companion object {
private const val ACTIVE_URL = "active_url"
private const val USER_CANCELLED = "user_cancelled"
private const val LINK_PREVIEW_STATE = "link_preview_state"
}
private var enabled = SignalStore.settings().isLinkPreviewsEnabled
private val linkPreviewStateStore = RxStore(savedStateHandle[LINK_PREVIEW_STATE] ?: LinkPreviewState.forNoLinks())
val linkPreviewState: Flowable<LinkPreviewState> = linkPreviewStateStore.stateFlowable.observeOn(AndroidSchedulers.mainThread())
val linkPreviewStateSnapshot: LinkPreviewState = linkPreviewStateStore.state
val hasLinkPreview: Boolean = linkPreviewStateStore.state.linkPreview.isPresent
val hasLinkPreviewUi: Boolean = linkPreviewStateStore.state.hasContent()
private var activeUrl: String? = null
private var activeUrl: String?
get() = savedStateHandle[ACTIVE_URL]
set(value) {
savedStateHandle[ACTIVE_URL] = value
}
private var userCancelled: Boolean
get() = savedStateHandle[USER_CANCELLED] ?: false
set(value) {
savedStateHandle[USER_CANCELLED] = value
}
private var activeRequest: Disposable = Disposable.disposed()
private var userCancelled: Boolean = false
private val debouncer: Debouncer = Debouncer(250)
private var savedStateDisposable: Disposable = linkPreviewStateStore
.stateFlowable
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy {
savedStateHandle[LINK_PREVIEW_STATE] = it
}
override fun onCleared() {
activeRequest.dispose()
savedStateDisposable.dispose()
debouncer.clear()
}
@@ -46,6 +74,7 @@ class LinkPreviewViewModelV2(
val currentState = linkPreviewStateStore.state
onUserCancel()
userCancelled = false
return currentState.linkPreview.map { listOf(it) }.orElse(emptyList())
}
@@ -143,7 +172,7 @@ class LinkPreviewViewModelV2(
}
}
private fun setLinkPreviewState(linkPreviewState: LinkPreviewState) {
fun setLinkPreviewState(linkPreviewState: LinkPreviewState) {
linkPreviewStateStore.update { cleanseState(linkPreviewState) }
}