Skip to content

Commit

Permalink
Add support for link preview descriptions.
Browse files Browse the repository at this point in the history
  • Loading branch information
greyson-signal committed Aug 25, 2020
1 parent a3438c4 commit c78e098
Show file tree
Hide file tree
Showing 19 changed files with 151 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ public void setCorners(int topLeft, int topRight, int bottomRight, int bottomLef
outliner.setRadii(topLeft, topRight, bottomRight, bottomLeft);
}

public void setMinimumThumbnailWidth(int width) {
thumbnail.setMinimumThumbnailWidth(width);
}

public void setBorderless(boolean borderless) {
this.borderless = borderless;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@
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 {

Expand All @@ -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;
Expand Down Expand Up @@ -63,6 +65,7 @@ private void init(@Nullable AttributeSet attrs) {
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);
Expand All @@ -85,6 +88,8 @@ private void init(@Nullable AttributeSet attrs) {
container.setPadding(0, 0, 0, 0);
divider.setVisibility(VISIBLE);
closeButton.setVisibility(VISIBLE);
title.setMaxLines(2);
description.setMaxLines(2);

closeButton.setOnClickListener(v -> {
if (closeClickedListener != null) {
Expand All @@ -108,6 +113,7 @@ protected void dispatchDraw(Canvas canvas) {
public void setLoading() {
title.setVisibility(GONE);
site.setVisibility(GONE);
description.setVisibility(GONE);
thumbnail.setVisibility(GONE);
spinner.setVisibility(VISIBLE);
noPreview.setVisibility(INVISIBLE);
Expand All @@ -123,17 +129,33 @@ public void setNoPreview(@Nullable LinkPreviewRepository.Error customError) {
}

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

if (!Util.isEmpty(linkPreview.getDescription())) {
description.setText(linkPreview.getDescription());
description.setVisibility(VISIBLE);
} else {
description.setVisibility(GONE);
}

HttpUrl url = HttpUrl.parse(linkPreview.getUrl());
if (url != null) {
site.setText(url.topPrivateDomain());
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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
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;
Expand Down Expand Up @@ -569,10 +570,17 @@ private boolean hasLinkPreview(MessageRecord messageRecord) {
}

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 &&
Expand Down Expand Up @@ -681,6 +689,7 @@ private void setMediaAttributes(@NonNull MessageRecord messageRecord,

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);
Expand Down Expand Up @@ -778,10 +787,12 @@ private void setMediaAttributes(@NonNull MessageRecord messageRecord,
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1118,7 +1118,7 @@ private static List<LinkPreview> getLinkPreviews(@NonNull Cursor cursor, @NonNul
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);
Expand Down Expand Up @@ -1526,7 +1526,7 @@ public void deleteThread(long threadId) {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1693,15 +1693,16 @@ private static Optional<List<LinkPreview>> getLinkPreviews(Optional<List<Preview
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));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ List<SharedContact> getSharedContactsFor(OutgoingMediaMessage mediaMessage) {
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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,57 +22,68 @@ 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;
}

public @Nullable AttachmentId getAttachmentId() {
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,15 @@ public 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;
}

RequestController imageController = fetchThumbnail(metadata.getImageUrl().get(), attachment -> {
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));
}
});

Expand Down Expand Up @@ -147,17 +147,18 @@ public void onResponse(@NonNull Call call, @NonNull Response response) throws IO
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));
}
});

Expand Down Expand Up @@ -225,7 +226,7 @@ private static RequestController fetchStickerPackLinkPreview(@NonNull Context co

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);
}
Expand Down Expand Up @@ -268,7 +269,7 @@ private static RequestController fetchGroupLinkPreview(@NonNull Context context,
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");

Expand All @@ -284,7 +285,7 @@ private static RequestController fetchGroupLinkPreview(@NonNull Context context,
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);
Expand Down Expand Up @@ -337,21 +338,27 @@ private static Optional<Attachment> bitmapToAttachment(@Nullable Bitmap bitmap,

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;
}
Expand Down

0 comments on commit c78e098

Please sign in to comment.