Add support for link preview descriptions.

master
Greyson Parrelli 2020-08-25 16:05:39 -04:00 committed by GitHub
parent a3438c4f8d
commit c78e098cb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 151 additions and 58 deletions

View File

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

View File

@ -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()) {

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -11,7 +11,7 @@
android:contentDescription="@string/conversation_item__mms_image_description"
android:visibility="gone"
android:elevation="8dp"
app:conversationThumbnail_minWidth="@dimen/media_bubble_min_width"
app:conversationThumbnail_minWidth="@dimen/media_bubble_min_width_solo"
app:conversationThumbnail_maxWidth="@dimen/media_bubble_max_width"
app:conversationThumbnail_minHeight="@dimen/media_bubble_min_height"
app:conversationThumbnail_maxHeight="@dimen/media_bubble_max_height"

View File

@ -13,7 +13,7 @@
android:contentDescription="@string/conversation_item__mms_image_description"
android:visibility="gone"
android:elevation="8dp"
app:conversationThumbnail_minWidth="@dimen/media_bubble_min_width"
app:conversationThumbnail_minWidth="@dimen/media_bubble_min_width_solo"
app:conversationThumbnail_maxWidth="@dimen/media_bubble_max_width"
app:conversationThumbnail_minHeight="@dimen/media_bubble_min_height"
app:conversationThumbnail_maxHeight="@dimen/media_bubble_max_height"

View File

@ -15,6 +15,7 @@
android:id="@+id/linkpreview_thumbnail"
android:layout_width="72dp"
android:layout_height="0dp"
android:maxHeight="72dp"
android:scaleType="centerCrop"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/linkpreview_divider"
@ -26,11 +27,11 @@
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/linkpreview_title"
style="@style/Signal.Text.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
style="@style/Signal.Text.Preview"
android:ellipsize="end"
android:fontFamily="sans-serif-medium"
android:maxLines="2"
@ -39,7 +40,23 @@
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/linkpreview_thumbnail"
app:layout_constraintTop_toTopOf="parent"
tools:text="Wall Crawler Strikes Again!" />
tools:text="J. Jonah Jameson on Twitter" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/linkpreview_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
style="@style/Signal.Text.Preview"
android:textSize="13sp"
android:ellipsize="end"
android:maxLines="15"
app:layout_constraintEnd_toStartOf="@+id/linkpreview_close"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/linkpreview_thumbnail"
app:layout_constraintTop_toBottomOf="@id/linkpreview_title"
tools:text="Wall crawler strikes again!" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/linkpreview_site"
@ -50,7 +67,7 @@
android:layout_marginTop="2dp"
android:textColor="?linkpreview_secondary_text_color"
app:layout_constraintStart_toEndOf="@+id/linkpreview_thumbnail"
app:layout_constraintTop_toBottomOf="@+id/linkpreview_title"
app:layout_constraintTop_toBottomOf="@+id/linkpreview_description"
tools:text="dailybugle.com" />
<View

View File

@ -19,7 +19,7 @@
android:layout_height="@dimen/media_bubble_sticker_dimens"
app:thumbnail_radius="0dp"
app:thumbnail_fit="fit_center"
app:minWidth="@dimen/media_bubble_min_width"
app:minWidth="@dimen/media_bubble_min_width_solo"
app:maxWidth="@dimen/media_bubble_max_width"
app:minHeight="@dimen/media_bubble_min_height"
app:maxHeight="@dimen/media_bubble_max_height" />

View File

@ -41,7 +41,8 @@
<dimen name="media_bubble_remove_button_size">24dp</dimen>
<dimen name="media_bubble_edit_button_size">24dp</dimen>
<dimen name="media_bubble_default_dimens">210dp</dimen>
<dimen name="media_bubble_min_width">150dp</dimen>
<dimen name="media_bubble_min_width_solo">150dp</dimen>
<dimen name="media_bubble_min_width_with_content">240dp</dimen>
<dimen name="media_bubble_max_width">240dp</dimen>
<dimen name="media_bubble_min_height">100dp</dimen>
<dimen name="media_bubble_max_height">320dp</dimen>

View File

@ -628,6 +628,7 @@ public class SignalServiceMessageSender {
for (SignalServiceDataMessage.Preview preview : message.getPreviews().get()) {
DataMessage.Preview.Builder previewBuilder = DataMessage.Preview.newBuilder();
previewBuilder.setTitle(preview.getTitle());
previewBuilder.setDescription(preview.getDescription());
previewBuilder.setUrl(preview.getUrl());
if (preview.getImage().isPresent()) {

View File

@ -685,8 +685,9 @@ public final class SignalServiceContent {
}
results.add(new SignalServiceDataMessage.Preview(preview.getUrl(),
preview.getTitle(),
Optional.fromNullable(attachment)));
preview.getTitle(),
preview.getDescription(),
Optional.fromNullable(attachment)));
}
return results;

View File

@ -412,12 +412,14 @@ public class SignalServiceDataMessage {
public static class Preview {
private final String url;
private final String title;
private final String description;
private final Optional<SignalServiceAttachment> image;
public Preview(String url, String title, Optional<SignalServiceAttachment> image) {
this.url = url;
this.title = title;
this.image = image;
public Preview(String url, String title, String description, Optional<SignalServiceAttachment> image) {
this.url = url;
this.title = title;
this.description = description;
this.image = image;
}
public String getUrl() {
@ -428,6 +430,10 @@ public class SignalServiceDataMessage {
return title;
}
public String getDescription() {
return description;
}
public Optional<SignalServiceAttachment> getImage() {
return image;
}

View File

@ -203,9 +203,10 @@ message DataMessage {
}
message Preview {
optional string url = 1;
optional string title = 2;
optional AttachmentPointer image = 3;
optional string url = 1;
optional string title = 2;
optional AttachmentPointer image = 3;
optional string description = 4;
}
message Sticker {