mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-30 17:36:23 +00:00
Add support for link preview descriptions.
This commit is contained in:
@@ -119,6 +119,10 @@ public class ConversationItemThumbnail extends FrameLayout {
|
||||
outliner.setRadii(topLeft, topRight, bottomRight, bottomLeft);
|
||||
}
|
||||
|
||||
public void setMinimumThumbnailWidth(int width) {
|
||||
thumbnail.setMinimumThumbnailWidth(width);
|
||||
}
|
||||
|
||||
public void setBorderless(boolean borderless) {
|
||||
this.borderless = borderless;
|
||||
}
|
||||
|
||||
@@ -21,11 +21,12 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
/**
|
||||
* The view shown in the compose box that represents the state of the link preview.
|
||||
* The view shown in the compose box or conversation that represents the state of the link preview.
|
||||
*/
|
||||
public class LinkPreviewView extends FrameLayout {
|
||||
|
||||
@@ -35,6 +36,7 @@ public class LinkPreviewView extends FrameLayout {
|
||||
private ViewGroup container;
|
||||
private OutlinedThumbnailView thumbnail;
|
||||
private TextView title;
|
||||
private TextView description;
|
||||
private TextView site;
|
||||
private View divider;
|
||||
private View closeButton;
|
||||
@@ -63,6 +65,7 @@ public class LinkPreviewView extends FrameLayout {
|
||||
container = findViewById(R.id.linkpreview_container);
|
||||
thumbnail = findViewById(R.id.linkpreview_thumbnail);
|
||||
title = findViewById(R.id.linkpreview_title);
|
||||
description = findViewById(R.id.linkpreview_description);
|
||||
site = findViewById(R.id.linkpreview_site);
|
||||
divider = findViewById(R.id.linkpreview_divider);
|
||||
spinner = findViewById(R.id.linkpreview_progress_wheel);
|
||||
@@ -85,6 +88,8 @@ public class LinkPreviewView extends FrameLayout {
|
||||
container.setPadding(0, 0, 0, 0);
|
||||
divider.setVisibility(VISIBLE);
|
||||
closeButton.setVisibility(VISIBLE);
|
||||
title.setMaxLines(2);
|
||||
description.setMaxLines(2);
|
||||
|
||||
closeButton.setOnClickListener(v -> {
|
||||
if (closeClickedListener != null) {
|
||||
@@ -108,6 +113,7 @@ public class LinkPreviewView extends FrameLayout {
|
||||
public void setLoading() {
|
||||
title.setVisibility(GONE);
|
||||
site.setVisibility(GONE);
|
||||
description.setVisibility(GONE);
|
||||
thumbnail.setVisibility(GONE);
|
||||
spinner.setVisibility(VISIBLE);
|
||||
noPreview.setVisibility(INVISIBLE);
|
||||
@@ -123,17 +129,33 @@ public class LinkPreviewView extends FrameLayout {
|
||||
}
|
||||
|
||||
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
|
||||
title.setVisibility(VISIBLE);
|
||||
site.setVisibility(VISIBLE);
|
||||
thumbnail.setVisibility(VISIBLE);
|
||||
spinner.setVisibility(GONE);
|
||||
noPreview.setVisibility(GONE);
|
||||
|
||||
title.setText(linkPreview.getTitle());
|
||||
if (!Util.isEmpty(linkPreview.getTitle())) {
|
||||
title.setText(linkPreview.getTitle());
|
||||
title.setVisibility(VISIBLE);
|
||||
} else {
|
||||
title.setVisibility(GONE);
|
||||
}
|
||||
|
||||
HttpUrl url = HttpUrl.parse(linkPreview.getUrl());
|
||||
if (url != null) {
|
||||
site.setText(url.topPrivateDomain());
|
||||
if (!Util.isEmpty(linkPreview.getDescription())) {
|
||||
description.setText(linkPreview.getDescription());
|
||||
description.setVisibility(VISIBLE);
|
||||
} else {
|
||||
description.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (!Util.isEmpty(linkPreview.getUrl())) {
|
||||
HttpUrl url = HttpUrl.parse(linkPreview.getUrl());
|
||||
if (url != null) {
|
||||
site.setText(url.topPrivateDomain());
|
||||
site.setVisibility(VISIBLE);
|
||||
} else {
|
||||
site.setVisibility(GONE);
|
||||
}
|
||||
} else {
|
||||
site.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
|
||||
|
||||
@@ -141,6 +141,11 @@ public class ThumbnailView extends FrameLayout {
|
||||
captionIcon.setScaleY(captionIconScale);
|
||||
}
|
||||
|
||||
public void setMinimumThumbnailWidth(int width) {
|
||||
bounds[MIN_WIDTH] = width;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
private void fillTargetDimensions(int[] targetDimens, int[] dimens, int[] bounds) {
|
||||
int dimensFilledCount = getNonZeroCount(dimens);
|
||||
|
||||
@@ -116,6 +116,7 @@ import org.thoughtcrime.securesms.util.LongClickMovementMethod;
|
||||
import org.thoughtcrime.securesms.util.SearchUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.VibrateUtil;
|
||||
import org.thoughtcrime.securesms.util.UrlClickHandler;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
@@ -569,10 +570,17 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
}
|
||||
|
||||
private boolean hasBigImageLinkPreview(MessageRecord messageRecord) {
|
||||
if (!hasLinkPreview(messageRecord)) return false;
|
||||
if (!hasLinkPreview(messageRecord)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0);
|
||||
int minWidth = getResources().getDimensionPixelSize(R.dimen.media_bubble_min_width);
|
||||
|
||||
if (linkPreview.getThumbnail().isPresent() && !Util.isEmpty(linkPreview.getDescription())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int minWidth = getResources().getDimensionPixelSize(R.dimen.media_bubble_min_width_solo);
|
||||
|
||||
return linkPreview.getThumbnail().isPresent() &&
|
||||
linkPreview.getThumbnail().get().getWidth() >= minWidth &&
|
||||
@@ -681,6 +689,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
|
||||
if (hasBigImageLinkPreview(messageRecord)) {
|
||||
mediaThumbnailStub.get().setVisibility(VISIBLE);
|
||||
mediaThumbnailStub.get().setMinimumThumbnailWidth(readDimen(R.dimen.media_bubble_min_width_with_content));
|
||||
mediaThumbnailStub.get().setImageResource(glideRequests, Collections.singletonList(new ImageSlide(context, linkPreview.getThumbnail().get())), showControls, false);
|
||||
mediaThumbnailStub.get().setThumbnailClickListener(new LinkPreviewThumbnailClickListener());
|
||||
mediaThumbnailStub.get().setDownloadClickListener(downloadClickListener);
|
||||
@@ -778,10 +787,12 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE);
|
||||
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
|
||||
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
|
||||
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
|
||||
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
|
||||
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
|
||||
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
|
||||
|
||||
List<Slide> thumbnailSlides = ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlides();
|
||||
mediaThumbnailStub.get().setMinimumThumbnailWidth(readDimen(isCaptionlessMms(messageRecord) ? R.dimen.media_bubble_min_width_solo
|
||||
: R.dimen.media_bubble_min_width_with_content));
|
||||
mediaThumbnailStub.get().setImageResource(glideRequests,
|
||||
thumbnailSlides,
|
||||
showControls,
|
||||
|
||||
@@ -1118,7 +1118,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
if (preview.getAttachmentId() != null) {
|
||||
DatabaseAttachment attachment = attachmentIdMap.get(preview.getAttachmentId());
|
||||
if (attachment != null) {
|
||||
previews.add(new LinkPreview(preview.getUrl(), preview.getTitle(), attachment));
|
||||
previews.add(new LinkPreview(preview.getUrl(), preview.getTitle(), preview.getDescription(), attachment));
|
||||
}
|
||||
} else {
|
||||
previews.add(preview);
|
||||
@@ -1526,7 +1526,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
attachmentId = insertedAttachmentIds.get(preview.getThumbnail().get());
|
||||
}
|
||||
|
||||
LinkPreview updatedPreview = new LinkPreview(preview.getUrl(), preview.getTitle(), attachmentId);
|
||||
LinkPreview updatedPreview = new LinkPreview(preview.getUrl(), preview.getTitle(), preview.getDescription(), attachmentId);
|
||||
linkPreviewJson.put(new JSONObject(updatedPreview.serialize()));
|
||||
} catch (JSONException | IOException e) {
|
||||
Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e);
|
||||
|
||||
@@ -1693,15 +1693,16 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||
Optional<Attachment> thumbnail = PointerAttachment.forPointer(preview.getImage());
|
||||
Optional<String> url = Optional.fromNullable(preview.getUrl());
|
||||
Optional<String> title = Optional.fromNullable(preview.getTitle());
|
||||
boolean hasContent = !TextUtils.isEmpty(title.or("")) || thumbnail.isPresent();
|
||||
Optional<String> description = Optional.fromNullable(preview.getDescription());
|
||||
boolean hasTitle = !TextUtils.isEmpty(title.or(""));
|
||||
boolean presentInBody = url.isPresent() && Stream.of(LinkPreviewUtil.findValidPreviewUrls(message)).map(Link::getUrl).collect(Collectors.toSet()).contains(url.get());
|
||||
boolean validDomain = url.isPresent() && LinkPreviewUtil.isValidPreviewUrl(url.get());
|
||||
|
||||
if (hasContent && presentInBody && validDomain) {
|
||||
LinkPreview linkPreview = new LinkPreview(url.get(), title.or(""), thumbnail);
|
||||
if (hasTitle && presentInBody && validDomain) {
|
||||
LinkPreview linkPreview = new LinkPreview(url.get(), title.or(""), description.or(""), thumbnail);
|
||||
linkPreviews.add(linkPreview);
|
||||
} else {
|
||||
Log.w(TAG, String.format("Discarding an invalid link preview. hasContent: %b presentInBody: %b validDomain: %b", hasContent, presentInBody, validDomain));
|
||||
Log.w(TAG, String.format("Discarding an invalid link preview. hasTitle: %b presentInBody: %b validDomain: %b", hasTitle, presentInBody, validDomain));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -315,7 +315,7 @@ public abstract class PushSendJob extends SendJob {
|
||||
List<Preview> getPreviewsFor(OutgoingMediaMessage mediaMessage) {
|
||||
return Stream.of(mediaMessage.getLinkPreviews()).map(lp -> {
|
||||
SignalServiceAttachment attachment = lp.getThumbnail().isPresent() ? getAttachmentPointerFor(lp.getThumbnail().get()) : null;
|
||||
return new Preview(lp.getUrl(), lp.getTitle(), Optional.fromNullable(attachment));
|
||||
return new Preview(lp.getUrl(), lp.getTitle(), lp.getDescription(), Optional.fromNullable(attachment));
|
||||
}).toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -22,45 +22,56 @@ public class LinkPreview {
|
||||
@JsonProperty
|
||||
private final String title;
|
||||
|
||||
@JsonProperty
|
||||
private final String description;
|
||||
|
||||
@JsonProperty
|
||||
private final AttachmentId attachmentId;
|
||||
|
||||
@JsonIgnore
|
||||
private final Optional<Attachment> thumbnail;
|
||||
|
||||
public LinkPreview(@NonNull String url, @NonNull String title, @NonNull DatabaseAttachment thumbnail) {
|
||||
public LinkPreview(@NonNull String url, @NonNull String title, @NonNull String description, @NonNull DatabaseAttachment thumbnail) {
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.thumbnail = Optional.of(thumbnail);
|
||||
this.attachmentId = thumbnail.getAttachmentId();
|
||||
}
|
||||
|
||||
public LinkPreview(@NonNull String url, @NonNull String title, @NonNull Optional<Attachment> thumbnail) {
|
||||
public LinkPreview(@NonNull String url, @NonNull String title, @NonNull String description, @NonNull Optional<Attachment> thumbnail) {
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.thumbnail = thumbnail;
|
||||
this.attachmentId = null;
|
||||
}
|
||||
|
||||
public LinkPreview(@JsonProperty("url") @NonNull String url,
|
||||
@JsonProperty("title") @NonNull String title,
|
||||
@JsonProperty("description") @Nullable String description,
|
||||
@JsonProperty("attachmentId") @Nullable AttachmentId attachmentId)
|
||||
{
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
this.description = Optional.fromNullable(description).or("");
|
||||
this.attachmentId = attachmentId;
|
||||
this.thumbnail = Optional.absent();
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
public @NonNull String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
public @NonNull String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public Optional<Attachment> getThumbnail() {
|
||||
public @NonNull String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public @NonNull Optional<Attachment> getThumbnail() {
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
@@ -68,11 +79,11 @@ public class LinkPreview {
|
||||
return attachmentId;
|
||||
}
|
||||
|
||||
public String serialize() throws IOException {
|
||||
public @NonNull String serialize() throws IOException {
|
||||
return JsonUtils.toJson(this);
|
||||
}
|
||||
|
||||
public static LinkPreview deserialize(@NonNull String serialized) throws IOException {
|
||||
public static @NonNull LinkPreview deserialize(@NonNull String serialized) throws IOException {
|
||||
return JsonUtils.fromJson(serialized, LinkPreview.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ public class LinkPreviewRepository {
|
||||
}
|
||||
|
||||
if (!metadata.getImageUrl().isPresent()) {
|
||||
callback.onSuccess(new LinkPreview(url, metadata.getTitle().get(), Optional.absent()));
|
||||
callback.onSuccess(new LinkPreview(url, metadata.getTitle().or(""), metadata.getDescription().or(""), Optional.absent()));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ public class LinkPreviewRepository {
|
||||
if (!metadata.getTitle().isPresent() && !attachment.isPresent()) {
|
||||
callback.onError(Error.PREVIEW_NOT_AVAILABLE);
|
||||
} else {
|
||||
callback.onSuccess(new LinkPreview(url, metadata.getTitle().or(""), attachment));
|
||||
callback.onSuccess(new LinkPreview(url, metadata.getTitle().or(""), metadata.getDescription().or(""), attachment));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -147,17 +147,18 @@ public class LinkPreviewRepository {
|
||||
return;
|
||||
}
|
||||
|
||||
String body = OkHttpUtil.readAsString(response.body(), FAILSAFE_MAX_TEXT_SIZE);
|
||||
OpenGraph openGraph = LinkPreviewUtil.parseOpenGraphFields(body);
|
||||
Optional<String> title = openGraph.getTitle();
|
||||
Optional<String> imageUrl = openGraph.getImageUrl();
|
||||
String body = OkHttpUtil.readAsString(response.body(), FAILSAFE_MAX_TEXT_SIZE);
|
||||
OpenGraph openGraph = LinkPreviewUtil.parseOpenGraphFields(body);
|
||||
Optional<String> title = openGraph.getTitle();
|
||||
Optional<String> description = openGraph.getDescription();
|
||||
Optional<String> imageUrl = openGraph.getImageUrl();
|
||||
|
||||
if (imageUrl.isPresent() && !LinkPreviewUtil.isValidPreviewUrl(imageUrl.get())) {
|
||||
Log.i(TAG, "Image URL was invalid or for a non-whitelisted domain. Skipping.");
|
||||
imageUrl = Optional.absent();
|
||||
}
|
||||
|
||||
callback.accept(new Metadata(title, imageUrl));
|
||||
callback.accept(new Metadata(title, description, imageUrl));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -225,7 +226,7 @@ public class LinkPreviewRepository {
|
||||
|
||||
Optional<Attachment> thumbnail = bitmapToAttachment(bitmap, Bitmap.CompressFormat.WEBP, MediaUtil.IMAGE_WEBP);
|
||||
|
||||
callback.onSuccess(new LinkPreview(packUrl, title, thumbnail));
|
||||
callback.onSuccess(new LinkPreview(packUrl, title, "", thumbnail));
|
||||
} else {
|
||||
callback.onError(Error.PREVIEW_NOT_AVAILABLE);
|
||||
}
|
||||
@@ -268,7 +269,7 @@ public class LinkPreviewRepository {
|
||||
thumbnail = bitmapToAttachment(bitmap, Bitmap.CompressFormat.WEBP, MediaUtil.IMAGE_WEBP);
|
||||
}
|
||||
|
||||
callback.onSuccess(new LinkPreview(groupUrl, title, thumbnail));
|
||||
callback.onSuccess(new LinkPreview(groupUrl, title, "", thumbnail));
|
||||
} else {
|
||||
Log.i(TAG, "Group is not locally available for preview generation, fetching from server");
|
||||
|
||||
@@ -284,7 +285,7 @@ public class LinkPreviewRepository {
|
||||
if (bitmap != null) bitmap.recycle();
|
||||
}
|
||||
|
||||
callback.onSuccess(new LinkPreview(groupUrl, joinInfo.getTitle(), thumbnail));
|
||||
callback.onSuccess(new LinkPreview(groupUrl, joinInfo.getTitle(), "", thumbnail));
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException | IOException | VerificationFailedException e) {
|
||||
Log.w(TAG, "Failed to fetch group link preview.", e);
|
||||
@@ -337,21 +338,27 @@ public class LinkPreviewRepository {
|
||||
|
||||
private static class Metadata {
|
||||
private final Optional<String> title;
|
||||
private final Optional<String> description;
|
||||
private final Optional<String> imageUrl;
|
||||
|
||||
Metadata(Optional<String> title, Optional<String> imageUrl) {
|
||||
this.title = title;
|
||||
this.imageUrl = imageUrl;
|
||||
Metadata(Optional<String> title, Optional<String> description, Optional<String> imageUrl) {
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
static Metadata empty() {
|
||||
return new Metadata(Optional.absent(), Optional.absent());
|
||||
return new Metadata(Optional.absent(), Optional.absent(), Optional.absent());
|
||||
}
|
||||
|
||||
Optional<String> getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
Optional<String> getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
Optional<String> getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
@@ -154,8 +154,9 @@ public final class LinkPreviewUtil {
|
||||
private final @Nullable String htmlTitle;
|
||||
private final @Nullable String faviconUrl;
|
||||
|
||||
private static final String KEY_TITLE = "title";
|
||||
private static final String KEY_IMAGE_URL = "image";
|
||||
private static final String KEY_TITLE = "title";
|
||||
private static final String KEY_DESCRIPTION_URL = "description";
|
||||
private static final String KEY_IMAGE_URL = "image";
|
||||
|
||||
public OpenGraph(@NonNull Map<String, String> values, @Nullable String htmlTitle, @Nullable String faviconUrl) {
|
||||
this.values = values;
|
||||
@@ -170,6 +171,10 @@ public final class LinkPreviewUtil {
|
||||
public @NonNull Optional<String> getImageUrl() {
|
||||
return OptionalUtil.absentIfEmpty(Util.getFirstNonEmpty(values.get(KEY_IMAGE_URL), faviconUrl));
|
||||
}
|
||||
|
||||
public @NonNull Optional<String> getDescription() {
|
||||
return OptionalUtil.absentIfEmpty(values.get(KEY_DESCRIPTION_URL));
|
||||
}
|
||||
}
|
||||
|
||||
public interface HtmlDecoder {
|
||||
|
||||
Reference in New Issue
Block a user