Don't preview links if your cursor is touching them.

This commit is contained in:
Greyson Parrelli 2019-02-14 13:55:48 -08:00
parent 1c23603c25
commit fe4068afce
6 changed files with 77 additions and 19 deletions

View File

@ -11,6 +11,7 @@ import android.support.v13.view.inputmethod.EditorInfoCompat;
import android.support.v13.view.inputmethod.InputConnectionCompat;
import android.support.v13.view.inputmethod.InputContentInfoCompat;
import android.support.v4.os.BuildCompat;
import android.text.Editable;
import android.text.InputType;
import android.text.Spannable;
import android.text.SpannableString;
@ -33,7 +34,8 @@ public class ComposeText extends EmojiEditText {
private CharSequence hint;
private SpannableString subHint;
@Nullable private InputPanel.MediaListener mediaListener;
@Nullable private InputPanel.MediaListener mediaListener;
@Nullable private CursorPositionChangedListener cursorPositionChangedListener;
public ComposeText(Context context) {
super(context);
@ -69,6 +71,15 @@ public class ComposeText extends EmojiEditText {
}
}
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
if (cursorPositionChangedListener != null) {
cursorPositionChangedListener.onCursorPositionChanged(selStart, selEnd);
}
}
private CharSequence ellipsizeToWidth(CharSequence text) {
return TextUtils.ellipsize(text,
getPaint(),
@ -104,6 +115,10 @@ public class ComposeText extends EmojiEditText {
setSelection(getText().length());
}
public void setCursorPositionChangedListener(@Nullable CursorPositionChangedListener listener) {
this.cursorPositionChangedListener = listener;
}
private boolean isLandscape() {
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
}
@ -189,4 +204,7 @@ public class ComposeText extends EmojiEditText {
}
}
public interface CursorPositionChangedListener {
void onCursorPositionChanged(int start, int end);
}
}

View File

@ -239,6 +239,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
AttachmentDrawerListener,
InputPanel.Listener,
InputPanel.MediaListener,
ComposeText.CursorPositionChangedListener,
ConversationSearchBottomBar.EventListener
{
private static final String TAG = ConversationActivity.class.getSimpleName();
@ -361,6 +362,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (TextSecurePreferences.isTypingIndicatorsEnabled(ConversationActivity.this)) {
composeText.addTextChangedListener(typingTextWatcher);
}
composeText.setSelection(composeText.length(), composeText.length());
}
});
}
@ -1527,6 +1529,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener();
composeText.setOnEditorActionListener(sendButtonListener);
composeText.setCursorPositionChangedListener(this);
attachButton.setOnClickListener(new AttachButtonListener());
attachButton.setOnLongClickListener(new AttachButtonLongClickListener());
sendButton.setOnClickListener(sendButtonListener);
@ -2187,7 +2190,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void updateLinkPreviewState() {
if (TextSecurePreferences.isLinkPreviewsEnabled(this) && !sendButton.getSelectedTransport().isSms() && !attachmentManager.isAttachmentPresent()) {
linkPreviewViewModel.onEnabled();
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed());
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), composeText.getSelectionStart(), composeText.getSelectionEnd());
} else {
linkPreviewViewModel.onUserCancel();
}
@ -2358,6 +2361,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
@Override
public void onCursorPositionChanged(int start, int end) {
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), start, end);
}
private void silentlySetComposeText(String text) {
typingTextWatcher.setEnabled(false);
composeText.setText(text);
@ -2461,11 +2469,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public void afterTextChanged(Editable s) {
calculateCharactersRemaining();
String trimmed = composeText.getTextTrimmed();
linkPreviewViewModel.onTextChanged(ConversationActivity.this, trimmed);
if (trimmed.length() == 0 || beforeLength == 0) {
if (composeText.getTextTrimmed().length() == 0 || beforeLength == 0) {
composeText.postDelayed(ConversationActivity.this::updateToggleButtonState, 50);
}
}

View File

@ -12,6 +12,7 @@ import android.support.v4.app.NotificationManagerCompat;
import android.text.TextUtils;
import android.util.Pair;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.signal.libsignal.metadata.InvalidMetadataMessageException;
@ -56,6 +57,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
import org.thoughtcrime.securesms.jobmanager.JobParameters;
import org.thoughtcrime.securesms.jobmanager.SafeData;
import org.thoughtcrime.securesms.linkpreview.Link;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log;
@ -1070,7 +1072,7 @@ public class PushDecryptJob extends ContextJob {
Optional<String> url = Optional.fromNullable(preview.getUrl());
Optional<String> title = Optional.fromNullable(preview.getTitle());
boolean hasContent = !TextUtils.isEmpty(title.or("")) || thumbnail.isPresent();
boolean presentInBody = url.isPresent() && LinkPreviewUtil.findWhitelistedUrls(message).contains(url.get());
boolean presentInBody = url.isPresent() && Stream.of(LinkPreviewUtil.findWhitelistedUrls(message)).map(Link::getUrl).collect(Collectors.toSet()).contains(url.get());
boolean validDomain = url.isPresent() && LinkPreviewUtil.isWhitelistedLinkUrl(url.get());
if (hasContent && presentInBody && validDomain) {

View File

@ -0,0 +1,20 @@
package org.thoughtcrime.securesms.linkpreview;
public class Link {
private final String url;
private final int position;
public Link(String url, int position) {
this.url = url;
this.position = position;
}
public String getUrl() {
return url;
}
public int getPosition() {
return position;
}
}

View File

@ -18,7 +18,7 @@ public final class LinkPreviewUtil {
/**
* @return All whitelisted URLs in the source text.
*/
public static @NonNull List<String> findWhitelistedUrls(@NonNull String text) {
public static @NonNull List<Link> findWhitelistedUrls(@NonNull String text) {
SpannableString spannable = new SpannableString(text);
boolean found = Linkify.addLinks(spannable, Linkify.WEB_URLS);
@ -27,8 +27,8 @@ public final class LinkPreviewUtil {
}
return Stream.of(spannable.getSpans(0, spannable.length(), URLSpan.class))
.map(URLSpan::getURL)
.filter(LinkPreviewUtil::isWhitelistedLinkUrl)
.map(span -> new Link(span.getURL(), spannable.getSpanStart(span)))
.filter(link -> isWhitelistedLinkUrl(link.getUrl()))
.toList();
}

View File

@ -7,6 +7,9 @@ import android.arch.lifecycle.ViewModelProvider;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import org.thoughtcrime.securesms.attachments.Attachment;
@ -81,7 +84,7 @@ public class LinkPreviewViewModel extends ViewModel {
return Collections.singletonList(new LinkPreview(originalPreview.getUrl(), originalPreview.getTitle(), Optional.of(newAttachment)));
}
public void onTextChanged(@NonNull Context context, @NonNull String text) {
public void onTextChanged(@NonNull Context context, @NonNull String text, int cursorStart, int cursorEnd) {
debouncer.publish(() -> {
if (TextUtils.isEmpty(text)) {
userCanceled = false;
@ -91,10 +94,10 @@ public class LinkPreviewViewModel extends ViewModel {
return;
}
List<String> urls = LinkPreviewUtil.findWhitelistedUrls(text);
Optional<String> url = urls.isEmpty() ? Optional.absent() : Optional.of(urls.get(0));
List<Link> links = LinkPreviewUtil.findWhitelistedUrls(text);
Optional<Link> link = links.isEmpty() ? Optional.absent() : Optional.of(links.get(0));
if (url.isPresent() && url.get().equals(activeUrl)) {
if (link.isPresent() && link.get().getUrl().equals(activeUrl)) {
return;
}
@ -103,7 +106,7 @@ public class LinkPreviewViewModel extends ViewModel {
activeRequest = null;
}
if (!url.isPresent()) {
if (!link.isPresent() || !isCursorPositionValid(text, link.get(), cursorStart, cursorEnd)) {
activeUrl = null;
linkPreviewState.setValue(LinkPreviewState.forEmpty());
return;
@ -111,8 +114,8 @@ public class LinkPreviewViewModel extends ViewModel {
linkPreviewState.setValue(LinkPreviewState.forLoading());
activeUrl = url.get();
activeRequest = repository.getLinkPreview(context, url.get(), lp -> {
activeUrl = link.get().getUrl();
activeRequest = repository.getLinkPreview(context, link.get().getUrl(), lp -> {
Util.runOnMain(() -> {
if (!userCanceled) {
linkPreviewState.setValue(LinkPreviewState.forPreview(lp));
@ -123,7 +126,6 @@ public class LinkPreviewViewModel extends ViewModel {
});
}
public void onUserCancel() {
if (activeRequest != null) {
activeRequest.cancel();
@ -150,6 +152,18 @@ public class LinkPreviewViewModel extends ViewModel {
debouncer.clear();
}
private boolean isCursorPositionValid(@NonNull String text, @NonNull Link link, int cursorStart, int cursorEnd) {
if (cursorStart != cursorEnd) {
return true;
}
if (text.endsWith(link.getUrl()) && cursorStart == link.getPosition() + link.getUrl().length()) {
return true;
}
return cursorStart < link.getPosition() || cursorStart > link.getPosition() + link.getUrl().length();
}
public static class LinkPreviewState {
private final boolean isLoading;
private final Optional<LinkPreview> linkPreview;