diff --git a/src/org/thoughtcrime/securesms/components/ComposeText.java b/src/org/thoughtcrime/securesms/components/ComposeText.java index bcef821698..bf592e347c 100644 --- a/src/org/thoughtcrime/securesms/components/ComposeText.java +++ b/src/org/thoughtcrime/securesms/components/ComposeText.java @@ -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); + } } diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 6dcdbeabaa..d07efde756 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -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); } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 7deb4f43ea..4c9455d1f0 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -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 url = Optional.fromNullable(preview.getUrl()); Optional 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) { diff --git a/src/org/thoughtcrime/securesms/linkpreview/Link.java b/src/org/thoughtcrime/securesms/linkpreview/Link.java new file mode 100644 index 0000000000..d58b370475 --- /dev/null +++ b/src/org/thoughtcrime/securesms/linkpreview/Link.java @@ -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; + } +} diff --git a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java index 41b8dfd18b..020868bdf5 100644 --- a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java +++ b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java @@ -18,7 +18,7 @@ public final class LinkPreviewUtil { /** * @return All whitelisted URLs in the source text. */ - public static @NonNull List findWhitelistedUrls(@NonNull String text) { + public static @NonNull List 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(); } diff --git a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java index fa7934d596..4380fa53b2 100644 --- a/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java +++ b/src/org/thoughtcrime/securesms/linkpreview/LinkPreviewViewModel.java @@ -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 urls = LinkPreviewUtil.findWhitelistedUrls(text); - Optional url = urls.isEmpty() ? Optional.absent() : Optional.of(urls.get(0)); + List links = LinkPreviewUtil.findWhitelistedUrls(text); + Optional 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;