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.InputConnectionCompat;
import android.support.v13.view.inputmethod.InputContentInfoCompat; import android.support.v13.view.inputmethod.InputContentInfoCompat;
import android.support.v4.os.BuildCompat; import android.support.v4.os.BuildCompat;
import android.text.Editable;
import android.text.InputType; import android.text.InputType;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableString; import android.text.SpannableString;
@ -33,7 +34,8 @@ public class ComposeText extends EmojiEditText {
private CharSequence hint; private CharSequence hint;
private SpannableString subHint; private SpannableString subHint;
@Nullable private InputPanel.MediaListener mediaListener; @Nullable private InputPanel.MediaListener mediaListener;
@Nullable private CursorPositionChangedListener cursorPositionChangedListener;
public ComposeText(Context context) { public ComposeText(Context context) {
super(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) { private CharSequence ellipsizeToWidth(CharSequence text) {
return TextUtils.ellipsize(text, return TextUtils.ellipsize(text,
getPaint(), getPaint(),
@ -104,6 +115,10 @@ public class ComposeText extends EmojiEditText {
setSelection(getText().length()); setSelection(getText().length());
} }
public void setCursorPositionChangedListener(@Nullable CursorPositionChangedListener listener) {
this.cursorPositionChangedListener = listener;
}
private boolean isLandscape() { private boolean isLandscape() {
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; 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, AttachmentDrawerListener,
InputPanel.Listener, InputPanel.Listener,
InputPanel.MediaListener, InputPanel.MediaListener,
ComposeText.CursorPositionChangedListener,
ConversationSearchBottomBar.EventListener ConversationSearchBottomBar.EventListener
{ {
private static final String TAG = ConversationActivity.class.getSimpleName(); private static final String TAG = ConversationActivity.class.getSimpleName();
@ -361,6 +362,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (TextSecurePreferences.isTypingIndicatorsEnabled(ConversationActivity.this)) { if (TextSecurePreferences.isTypingIndicatorsEnabled(ConversationActivity.this)) {
composeText.addTextChangedListener(typingTextWatcher); composeText.addTextChangedListener(typingTextWatcher);
} }
composeText.setSelection(composeText.length(), composeText.length());
} }
}); });
} }
@ -1527,6 +1529,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener(); ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener();
composeText.setOnEditorActionListener(sendButtonListener); composeText.setOnEditorActionListener(sendButtonListener);
composeText.setCursorPositionChangedListener(this);
attachButton.setOnClickListener(new AttachButtonListener()); attachButton.setOnClickListener(new AttachButtonListener());
attachButton.setOnLongClickListener(new AttachButtonLongClickListener()); attachButton.setOnLongClickListener(new AttachButtonLongClickListener());
sendButton.setOnClickListener(sendButtonListener); sendButton.setOnClickListener(sendButtonListener);
@ -2187,7 +2190,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void updateLinkPreviewState() { private void updateLinkPreviewState() {
if (TextSecurePreferences.isLinkPreviewsEnabled(this) && !sendButton.getSelectedTransport().isSms() && !attachmentManager.isAttachmentPresent()) { if (TextSecurePreferences.isLinkPreviewsEnabled(this) && !sendButton.getSelectedTransport().isSms() && !attachmentManager.isAttachmentPresent()) {
linkPreviewViewModel.onEnabled(); linkPreviewViewModel.onEnabled();
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed()); linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), composeText.getSelectionStart(), composeText.getSelectionEnd());
} else { } else {
linkPreviewViewModel.onUserCancel(); 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) { private void silentlySetComposeText(String text) {
typingTextWatcher.setEnabled(false); typingTextWatcher.setEnabled(false);
composeText.setText(text); composeText.setText(text);
@ -2461,11 +2469,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public void afterTextChanged(Editable s) { public void afterTextChanged(Editable s) {
calculateCharactersRemaining(); calculateCharactersRemaining();
String trimmed = composeText.getTextTrimmed(); if (composeText.getTextTrimmed().length() == 0 || beforeLength == 0) {
linkPreviewViewModel.onTextChanged(ConversationActivity.this, trimmed);
if (trimmed.length() == 0 || beforeLength == 0) {
composeText.postDelayed(ConversationActivity.this::updateToggleButtonState, 50); composeText.postDelayed(ConversationActivity.this::updateToggleButtonState, 50);
} }
} }

View File

@ -12,6 +12,7 @@ import android.support.v4.app.NotificationManagerCompat;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Pair; import android.util.Pair;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.signal.libsignal.metadata.InvalidMetadataMessageException; 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.groups.GroupMessageProcessor;
import org.thoughtcrime.securesms.jobmanager.JobParameters; import org.thoughtcrime.securesms.jobmanager.JobParameters;
import org.thoughtcrime.securesms.jobmanager.SafeData; import org.thoughtcrime.securesms.jobmanager.SafeData;
import org.thoughtcrime.securesms.linkpreview.Link;
import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
@ -1070,7 +1072,7 @@ public class PushDecryptJob extends ContextJob {
Optional<String> url = Optional.fromNullable(preview.getUrl()); Optional<String> url = Optional.fromNullable(preview.getUrl());
Optional<String> title = Optional.fromNullable(preview.getTitle()); Optional<String> title = Optional.fromNullable(preview.getTitle());
boolean hasContent = !TextUtils.isEmpty(title.or("")) || thumbnail.isPresent(); 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()); boolean validDomain = url.isPresent() && LinkPreviewUtil.isWhitelistedLinkUrl(url.get());
if (hasContent && presentInBody && validDomain) { 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. * @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); SpannableString spannable = new SpannableString(text);
boolean found = Linkify.addLinks(spannable, Linkify.WEB_URLS); 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)) return Stream.of(spannable.getSpans(0, spannable.length(), URLSpan.class))
.map(URLSpan::getURL) .map(span -> new Link(span.getURL(), spannable.getSpanStart(span)))
.filter(LinkPreviewUtil::isWhitelistedLinkUrl) .filter(link -> isWhitelistedLinkUrl(link.getUrl()))
.toList(); .toList();
} }

View File

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