Add support for article dates in link previews.
This commit is contained in:
parent
bfed03b7b5
commit
dd8b9ff8fb
13 changed files with 124 additions and 23 deletions
|
@ -23,6 +23,10 @@ import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
|||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
/**
|
||||
|
@ -146,14 +150,24 @@ public class LinkPreviewView extends FrameLayout {
|
|||
description.setVisibility(GONE);
|
||||
}
|
||||
|
||||
String domain = null;
|
||||
|
||||
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);
|
||||
domain = url.topPrivateDomain();
|
||||
}
|
||||
}
|
||||
|
||||
if (domain != null && linkPreview.getDate() > 0) {
|
||||
site.setText(getContext().getString(R.string.LinkPreviewView_domain_date, domain, formatDate(linkPreview.getDate())));
|
||||
site.setVisibility(VISIBLE);
|
||||
} else if (domain != null) {
|
||||
site.setText(domain);
|
||||
site.setVisibility(VISIBLE);
|
||||
} else if (linkPreview.getDate() > 0) {
|
||||
site.setText(formatDate(linkPreview.getDate()));
|
||||
site.setVisibility(VISIBLE);
|
||||
} else {
|
||||
site.setVisibility(GONE);
|
||||
}
|
||||
|
@ -187,6 +201,11 @@ public class LinkPreviewView extends FrameLayout {
|
|||
: R.string.LinkPreviewView_no_link_preview_available;
|
||||
}
|
||||
|
||||
private static String formatDate(long date) {
|
||||
DateFormat dateFormat = new SimpleDateFormat("MMM dd, yyyy", Locale.getDefault());
|
||||
return dateFormat.format(date);
|
||||
}
|
||||
|
||||
public interface CloseClickedListener {
|
||||
void onCloseClicked();
|
||||
}
|
||||
|
|
|
@ -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(), preview.getDescription(), attachment));
|
||||
previews.add(new LinkPreview(preview.getUrl(), preview.getTitle(), preview.getDescription(), preview.getDate(), 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(), preview.getDescription(), attachmentId);
|
||||
LinkPreview updatedPreview = new LinkPreview(preview.getUrl(), preview.getTitle(), preview.getDescription(), preview.getDate(), attachmentId);
|
||||
linkPreviewJson.put(new JSONObject(updatedPreview.serialize()));
|
||||
} catch (JSONException | IOException e) {
|
||||
Log.w(TAG, "Failed to serialize shared contact. Skipping it.", e);
|
||||
|
|
|
@ -1696,7 +1696,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
boolean validDomain = url.isPresent() && LinkPreviewUtil.isValidPreviewUrl(url.get());
|
||||
|
||||
if (hasTitle && presentInBody && validDomain) {
|
||||
LinkPreview linkPreview = new LinkPreview(url.get(), title.or(""), description.or(""), thumbnail);
|
||||
LinkPreview linkPreview = new LinkPreview(url.get(), title.or(""), description.or(""), preview.getDate(), thumbnail);
|
||||
linkPreviews.add(linkPreview);
|
||||
} else {
|
||||
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(), lp.getDescription(), Optional.fromNullable(attachment));
|
||||
return new Preview(lp.getUrl(), lp.getTitle(), lp.getDescription(), lp.getDate(), Optional.fromNullable(attachment));
|
||||
}).toList();
|
||||
}
|
||||
|
||||
|
|
|
@ -25,24 +25,29 @@ public class LinkPreview {
|
|||
@JsonProperty
|
||||
private final String description;
|
||||
|
||||
@JsonProperty
|
||||
private final long date;
|
||||
|
||||
@JsonProperty
|
||||
private final AttachmentId attachmentId;
|
||||
|
||||
@JsonIgnore
|
||||
private final Optional<Attachment> thumbnail;
|
||||
|
||||
public LinkPreview(@NonNull String url, @NonNull String title, @NonNull String description, @NonNull DatabaseAttachment thumbnail) {
|
||||
public LinkPreview(@NonNull String url, @NonNull String title, @NonNull String description, long date, @NonNull DatabaseAttachment thumbnail) {
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.date = date;
|
||||
this.thumbnail = Optional.of(thumbnail);
|
||||
this.attachmentId = thumbnail.getAttachmentId();
|
||||
}
|
||||
|
||||
public LinkPreview(@NonNull String url, @NonNull String title, @NonNull String description, @NonNull Optional<Attachment> thumbnail) {
|
||||
public LinkPreview(@NonNull String url, @NonNull String title, @NonNull String description, long date, @NonNull Optional<Attachment> thumbnail) {
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.date = date;
|
||||
this.thumbnail = thumbnail;
|
||||
this.attachmentId = null;
|
||||
}
|
||||
|
@ -50,11 +55,13 @@ public class LinkPreview {
|
|||
public LinkPreview(@JsonProperty("url") @NonNull String url,
|
||||
@JsonProperty("title") @NonNull String title,
|
||||
@JsonProperty("description") @Nullable String description,
|
||||
@JsonProperty("date") long date,
|
||||
@JsonProperty("attachmentId") @Nullable AttachmentId attachmentId)
|
||||
{
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
this.description = Optional.fromNullable(description).or("");
|
||||
this.date = date;
|
||||
this.attachmentId = attachmentId;
|
||||
this.thumbnail = Optional.absent();
|
||||
}
|
||||
|
@ -71,6 +78,10 @@ public class LinkPreview {
|
|||
return description;
|
||||
}
|
||||
|
||||
public long getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public @NonNull Optional<Attachment> getThumbnail() {
|
||||
return thumbnail;
|
||||
}
|
||||
|
|
|
@ -106,7 +106,7 @@ public class LinkPreviewRepository {
|
|||
}
|
||||
|
||||
if (!metadata.getImageUrl().isPresent()) {
|
||||
callback.onSuccess(new LinkPreview(url, metadata.getTitle().or(""), metadata.getDescription().or(""), Optional.absent()));
|
||||
callback.onSuccess(new LinkPreview(url, metadata.getTitle().or(""), metadata.getDescription().or(""), metadata.getDate(), Optional.absent()));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -114,7 +114,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(""), metadata.getDescription().or(""), attachment));
|
||||
callback.onSuccess(new LinkPreview(url, metadata.getTitle().or(""), metadata.getDescription().or(""), metadata.getDate(), attachment));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -153,13 +153,14 @@ public class LinkPreviewRepository {
|
|||
Optional<String> title = openGraph.getTitle();
|
||||
Optional<String> description = openGraph.getDescription();
|
||||
Optional<String> imageUrl = openGraph.getImageUrl();
|
||||
long date = openGraph.getDate();
|
||||
|
||||
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, description, imageUrl));
|
||||
callback.accept(new Metadata(title, description, date, imageUrl));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -227,7 +228,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, "", 0, thumbnail));
|
||||
} else {
|
||||
callback.onError(Error.PREVIEW_NOT_AVAILABLE);
|
||||
}
|
||||
|
@ -272,7 +273,7 @@ public class LinkPreviewRepository {
|
|||
thumbnail = bitmapToAttachment(bitmap, Bitmap.CompressFormat.WEBP, MediaUtil.IMAGE_WEBP);
|
||||
}
|
||||
|
||||
callback.onSuccess(new LinkPreview(groupUrl, title, description, thumbnail));
|
||||
callback.onSuccess(new LinkPreview(groupUrl, title, description, 0, thumbnail));
|
||||
} else {
|
||||
Log.i(TAG, "Group is not locally available for preview generation, fetching from server");
|
||||
|
||||
|
@ -289,7 +290,7 @@ public class LinkPreviewRepository {
|
|||
if (bitmap != null) bitmap.recycle();
|
||||
}
|
||||
|
||||
callback.onSuccess(new LinkPreview(groupUrl, joinInfo.getTitle(), description, thumbnail));
|
||||
callback.onSuccess(new LinkPreview(groupUrl, joinInfo.getTitle(), description, 0, thumbnail));
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException | IOException | VerificationFailedException e) {
|
||||
Log.w(TAG, "Failed to fetch group link preview.", e);
|
||||
|
@ -350,16 +351,18 @@ public class LinkPreviewRepository {
|
|||
private static class Metadata {
|
||||
private final Optional<String> title;
|
||||
private final Optional<String> description;
|
||||
private final long date;
|
||||
private final Optional<String> imageUrl;
|
||||
|
||||
Metadata(Optional<String> title, Optional<String> description, Optional<String> imageUrl) {
|
||||
Metadata(Optional<String> title, Optional<String> description, long date, Optional<String> imageUrl) {
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.date = date;
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
static Metadata empty() {
|
||||
return new Metadata(Optional.absent(), Optional.absent(), Optional.absent());
|
||||
return new Metadata(Optional.absent(), Optional.absent(), 0, Optional.absent());
|
||||
}
|
||||
|
||||
Optional<String> getTitle() {
|
||||
|
@ -370,6 +373,10 @@ public class LinkPreviewRepository {
|
|||
return description;
|
||||
}
|
||||
|
||||
long getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
Optional<String> getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
|
|
@ -13,14 +13,19 @@ import android.text.util.Linkify;
|
|||
import com.annimon.stream.Stream;
|
||||
import com.google.android.collect.Sets;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.stickers.StickerUrl;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
|
@ -30,10 +35,13 @@ import okhttp3.HttpUrl;
|
|||
|
||||
public final class LinkPreviewUtil {
|
||||
|
||||
private static final String TAG = Log.tag(LinkPreviewUtil.class);
|
||||
|
||||
private static final Pattern DOMAIN_PATTERN = Pattern.compile("^(https?://)?([^/]+).*$");
|
||||
private static final Pattern ALL_ASCII_PATTERN = Pattern.compile("^[\\x00-\\x7F]*$");
|
||||
private static final Pattern ALL_NON_ASCII_PATTERN = Pattern.compile("^[^\\x00-\\x7F]*$");
|
||||
private static final Pattern OPEN_GRAPH_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*og:([^\"]+)\"[^>]*/?\\s*>");
|
||||
private static final Pattern ARTICLE_TAG_PATTERN = Pattern.compile("<\\s*meta[^>]*property\\s*=\\s*\"\\s*article:([^\"]+)\"[^>]*/?\\s*>");
|
||||
private static final Pattern OPEN_GRAPH_CONTENT_PATTERN = Pattern.compile("content\\s*=\\s*\"([^\"]*)\"");
|
||||
private static final Pattern TITLE_PATTERN = Pattern.compile("<\\s*title[^>]*>(.*)<\\s*/title[^>]*>");
|
||||
private static final Pattern FAVICON_PATTERN = Pattern.compile("<\\s*link[^>]*rel\\s*=\\s*\".*icon.*\"[^>]*>");
|
||||
|
@ -112,7 +120,22 @@ public final class LinkPreviewUtil {
|
|||
Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag);
|
||||
if (contentMatcher.find() && contentMatcher.groupCount() > 0) {
|
||||
String content = htmlDecoder.fromEncoded(contentMatcher.group(1));
|
||||
openGraphTags.put(property, content);
|
||||
openGraphTags.put(property.toLowerCase(), content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Matcher articleMatcher = ARTICLE_TAG_PATTERN.matcher(html);
|
||||
|
||||
while (articleMatcher.find()) {
|
||||
String tag = articleMatcher.group();
|
||||
String property = articleMatcher.groupCount() > 0 ? articleMatcher.group(1) : null;
|
||||
|
||||
if (property != null) {
|
||||
Matcher contentMatcher = OPEN_GRAPH_CONTENT_PATTERN.matcher(tag);
|
||||
if (contentMatcher.find() && contentMatcher.groupCount() > 0) {
|
||||
String content = htmlDecoder.fromEncoded(contentMatcher.group(1));
|
||||
openGraphTags.put(property.toLowerCase(), content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -154,9 +177,13 @@ 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_DESCRIPTION_URL = "description";
|
||||
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";
|
||||
private static final String KEY_PUBLISHED_TIME_1 = "published_time";
|
||||
private static final String KEY_PUBLISHED_TIME_2 = "article:published_time";
|
||||
private static final String KEY_MODIFIED_TIME_1 = "modified_time";
|
||||
private static final String KEY_MODIFIED_TIME_2 = "article:modified_time";
|
||||
|
||||
public OpenGraph(@NonNull Map<String, String> values, @Nullable String htmlTitle, @Nullable String faviconUrl) {
|
||||
this.values = values;
|
||||
|
@ -172,9 +199,35 @@ public final class LinkPreviewUtil {
|
|||
return OptionalUtil.absentIfEmpty(Util.getFirstNonEmpty(values.get(KEY_IMAGE_URL), faviconUrl));
|
||||
}
|
||||
|
||||
public long getDate() {
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault());
|
||||
|
||||
return Stream.of(values.get(KEY_PUBLISHED_TIME_1),
|
||||
values.get(KEY_PUBLISHED_TIME_2),
|
||||
values.get(KEY_MODIFIED_TIME_1),
|
||||
values.get(KEY_MODIFIED_TIME_2))
|
||||
.map(dateString -> parseDate(format, dateString))
|
||||
.filter(time -> time > 0)
|
||||
.findFirst()
|
||||
.orElse(0L);
|
||||
}
|
||||
|
||||
public @NonNull Optional<String> getDescription() {
|
||||
return OptionalUtil.absentIfEmpty(values.get(KEY_DESCRIPTION_URL));
|
||||
}
|
||||
|
||||
private static long parseDate(DateFormat dateFormat, String dateString) {
|
||||
if (Util.isEmpty(dateString)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
return dateFormat.parse(dateString).getTime();
|
||||
} catch (ParseException e) {
|
||||
Log.w(TAG, "Failed to parse date.", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface HtmlDecoder {
|
||||
|
|
|
@ -66,6 +66,7 @@
|
|||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textColor="?linkpreview_secondary_text_color"
|
||||
android:maxLines="2"
|
||||
app:layout_constraintStart_toEndOf="@+id/linkpreview_thumbnail"
|
||||
app:layout_constraintTop_toBottomOf="@+id/linkpreview_description"
|
||||
tools:text="dailybugle.com" />
|
||||
|
|
|
@ -497,6 +497,7 @@
|
|||
<!-- LinkPreviewView -->
|
||||
<string name="LinkPreviewView_no_link_preview_available">No link preview available</string>
|
||||
<string name="LinkPreviewView_this_group_link_is_not_active">This group link is not active</string>
|
||||
<string name="LinkPreviewView_domain_date">%1$s · %2$s</string>
|
||||
|
||||
<!-- LinkPreviewRepository -->
|
||||
<plurals name="LinkPreviewRepository_d_members">
|
||||
|
|
|
@ -629,6 +629,7 @@ public class SignalServiceMessageSender {
|
|||
DataMessage.Preview.Builder previewBuilder = DataMessage.Preview.newBuilder();
|
||||
previewBuilder.setTitle(preview.getTitle());
|
||||
previewBuilder.setDescription(preview.getDescription());
|
||||
previewBuilder.setDate(preview.getDate());
|
||||
previewBuilder.setUrl(preview.getUrl());
|
||||
|
||||
if (preview.getImage().isPresent()) {
|
||||
|
|
|
@ -687,6 +687,7 @@ public final class SignalServiceContent {
|
|||
results.add(new SignalServiceDataMessage.Preview(preview.getUrl(),
|
||||
preview.getTitle(),
|
||||
preview.getDescription(),
|
||||
preview.getDate(),
|
||||
Optional.fromNullable(attachment)));
|
||||
}
|
||||
|
||||
|
|
|
@ -413,12 +413,14 @@ public class SignalServiceDataMessage {
|
|||
private final String url;
|
||||
private final String title;
|
||||
private final String description;
|
||||
private final long date;
|
||||
private final Optional<SignalServiceAttachment> image;
|
||||
|
||||
public Preview(String url, String title, String description, Optional<SignalServiceAttachment> image) {
|
||||
public Preview(String url, String title, String description, long date, Optional<SignalServiceAttachment> image) {
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.date = date;
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
|
@ -434,6 +436,10 @@ public class SignalServiceDataMessage {
|
|||
return description;
|
||||
}
|
||||
|
||||
public long getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public Optional<SignalServiceAttachment> getImage() {
|
||||
return image;
|
||||
}
|
||||
|
|
|
@ -207,6 +207,7 @@ message DataMessage {
|
|||
optional string title = 2;
|
||||
optional AttachmentPointer image = 3;
|
||||
optional string description = 4;
|
||||
optional uint64 date = 5;
|
||||
}
|
||||
|
||||
message Sticker {
|
||||
|
|
Loading…
Add table
Reference in a new issue