diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 7cb31b33e..68ab0fa51 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -75,6 +75,7 @@ import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; @@ -3263,6 +3264,16 @@ public class XmppConnectionService extends Service { return null; } + public Conversation findUniqueConversationByJid(XmppUri xmppUri) { + List findings = new ArrayList<>(); + for (Conversation c : getConversations()) { + if (c.getJid().asBareJid().equals(xmppUri.getJid()) && ((c.getMode() == Conversational.MODE_MULTI) == xmppUri.isAction(XmppUri.ACTION_JOIN))) { + findings.add(c); + } + } + return findings.size() == 1 ? findings.get(0) : null; + } + public boolean markRead(final Conversation conversation, boolean dismiss) { return markRead(conversation, null, dismiss).size() > 0; } diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 19fe52c2b..80f6bea10 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -13,6 +13,8 @@ import android.os.AsyncTask; import android.os.Bundle; import android.support.v7.app.AlertDialog; import android.support.v7.widget.Toolbar; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; import android.util.Log; import android.view.ContextMenu; import android.view.LayoutInflater; @@ -46,6 +48,8 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate; import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; +import eu.siacs.conversations.ui.util.MyLinkify; +import eu.siacs.conversations.ui.widget.ClickableMovementMethod; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.XmppUri; import rocks.xmpp.addr.Jid; @@ -547,7 +551,10 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers this.binding.mucTitle.setVisibility(View.GONE); } if (printableValue(subject)) { - this.binding.mucSubject.setText(mucOptions.getSubject()); + SpannableStringBuilder spannable = new SpannableStringBuilder(subject); + MyLinkify.addLinks(spannable, false); + this.binding.mucSubject.setText(spannable); + this.binding.mucSubject.setAutoLinkMask(0); this.binding.mucSubject.setVisibility(View.VISIBLE); } else { this.binding.mucSubject.setVisibility(View.GONE); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index 2e4987281..1776777fb 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -76,6 +76,7 @@ import eu.siacs.conversations.ui.util.ConversationMenuConfigurator; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.utils.ExceptionHelper; +import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP; @@ -446,6 +447,18 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio } } + public boolean onXmppUriClicked(Uri uri) { + XmppUri xmppUri = new XmppUri(uri); + if (xmppUri.isJidValid() && !xmppUri.hasFingerprints()) { + final Conversation conversation = xmppConnectionService.findUniqueConversationByJid(xmppUri); + if (conversation != null) { + openConversation(conversation, null); + return true; + } + } + return false; + } + @Override public boolean onOptionsItemSelected(MenuItem item) { if (MenuDoubleTabUtil.shouldIgnoreTap()) { diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index e8fc39cdc..fcd54c4cb 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -27,7 +27,6 @@ import android.text.format.DateUtils; import android.text.style.ForegroundColorSpan; import android.text.style.RelativeSizeSpan; import android.text.style.StyleSpan; -import android.text.util.Linkify; import android.util.DisplayMetrics; import android.util.Log; import android.view.ActionMode; @@ -47,7 +46,6 @@ import android.widget.Toast; import java.lang.ref.WeakReference; import java.net.URL; import java.util.List; -import java.util.Locale; import java.util.concurrent.RejectedExecutionException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -71,8 +69,8 @@ import eu.siacs.conversations.ui.ConversationFragment; import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.ui.service.AudioPlayer; import eu.siacs.conversations.ui.text.DividerSpan; -import eu.siacs.conversations.ui.text.FixedURLSpan; import eu.siacs.conversations.ui.text.QuoteSpan; +import eu.siacs.conversations.ui.util.MyLinkify; import eu.siacs.conversations.ui.widget.ClickableMovementMethod; import eu.siacs.conversations.ui.widget.CopyTextView; import eu.siacs.conversations.ui.widget.ListSelectionManager; @@ -80,10 +78,8 @@ import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.EmojiWrapper; import eu.siacs.conversations.utils.Emoticons; import eu.siacs.conversations.utils.GeoHelper; -import eu.siacs.conversations.utils.Patterns; import eu.siacs.conversations.utils.StylingHelper; import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xmpp.mam.MamReference; public class MessageAdapter extends ArrayAdapter implements CopyTextView.CopyHandler { @@ -93,57 +89,6 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie private static final int RECEIVED = 1; private static final int STATUS = 2; private static final int DATE_SEPARATOR = 3; - private static final Linkify.TransformFilter WEBURL_TRANSFORM_FILTER = (matcher, url) -> { - if (url == null) { - return null; - } - final String lcUrl = url.toLowerCase(Locale.US); - if (lcUrl.startsWith("http://") || lcUrl.startsWith("https://")) { - return removeTrailingBracket(url); - } else { - return "http://" + removeTrailingBracket(url); - } - }; - - private static String removeTrailingBracket(final String url) { - int numOpenBrackets = 0; - for (char c : url.toCharArray()) { - if (c == '(') { - ++numOpenBrackets; - } else if (c == ')') { - --numOpenBrackets; - } - } - if (numOpenBrackets != 0 && url.charAt(url.length() - 1) == ')') { - return url.substring(0, url.length() - 1); - } else { - return url; - } - } - - private static final Linkify.MatchFilter WEBURL_MATCH_FILTER = (cs, start, end) -> { - if (start > 0) { - if (cs.charAt(start - 1) == '@' || cs.charAt(start - 1) == '.' - || cs.subSequence(Math.max(0, start - 3), start).equals("://")) { - return false; - } - } - - if (end < cs.length()) { - // Reject strings that were probably matched only because they contain a dot followed by - // by some known TLD (see also comment for WORD_BOUNDARY in Patterns.java) - if (Character.isAlphabetic(cs.charAt(end-1)) && Character.isAlphabetic(cs.charAt(end))) { - return false; - } - } - - return true; - }; - - private static final Linkify.MatchFilter XMPPURI_MATCH_FILTER = (s, start, end) -> { - XmppUri uri = new XmppUri(s.subSequence(start, end).toString()); - return uri.isJidValid(); - }; private final XmppActivity activity; private final ListSelectionManager listSelectionManager = new ListSelectionManager(); private final AudioPlayer audioPlayer; @@ -567,11 +512,7 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie if (highlightedTerm != null) { StylingHelper.highlight(activity, body, highlightedTerm, StylingHelper.isDarkText(viewHolder.messageBody)); } - - Linkify.addLinks(body, Patterns.XMPP_PATTERN, "xmpp", XMPPURI_MATCH_FILTER, null); - Linkify.addLinks(body, Patterns.AUTOLINK_WEB_URL, "http", WEBURL_MATCH_FILTER, WEBURL_TRANSFORM_FILTER); - Linkify.addLinks(body, GeoHelper.GEO_URI, "geo"); - FixedURLSpan.fix(body); + MyLinkify.addLinks(body,true); viewHolder.messageBody.setAutoLinkMask(0); viewHolder.messageBody.setText(EmojiWrapper.transform(body)); viewHolder.messageBody.setTextIsSelectable(true); diff --git a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java b/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java index 9b8c3e8b8..f006f2a47 100644 --- a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java +++ b/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java @@ -38,10 +38,14 @@ import android.os.Build; import android.text.Editable; import android.text.Spanned; import android.text.style.URLSpan; +import android.util.Log; import android.view.View; import android.widget.Toast; +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.ui.ConversationsActivity; +import eu.siacs.conversations.utils.XmppUri; @SuppressLint("ParcelCreator") @@ -64,6 +68,12 @@ public class FixedURLSpan extends URLSpan { public void onClick(View widget) { final Uri uri = Uri.parse(getURL()); final Context context = widget.getContext(); + if (uri.getScheme().equals("xmpp") && context instanceof ConversationsActivity) { + if (((ConversationsActivity) context).onXmppUriClicked(uri)) { + widget.playSoundEffect(0); + return; + } + } final Intent intent = new Intent(Intent.ACTION_VIEW, uri); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); @@ -71,6 +81,7 @@ public class FixedURLSpan extends URLSpan { //intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); try { context.startActivity(intent); + widget.playSoundEffect(0); } catch (ActivityNotFoundException e) { Toast.makeText(context, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT).show(); } diff --git a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java new file mode 100644 index 000000000..d350e51f9 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2018, Daniel Gultsch All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package eu.siacs.conversations.ui.util; + +import android.text.Editable; +import android.text.util.Linkify; + +import java.util.Locale; + +import eu.siacs.conversations.ui.text.FixedURLSpan; +import eu.siacs.conversations.utils.GeoHelper; +import eu.siacs.conversations.utils.Patterns; +import eu.siacs.conversations.utils.XmppUri; + +public class MyLinkify { + + private static final Linkify.TransformFilter WEBURL_TRANSFORM_FILTER = (matcher, url) -> { + if (url == null) { + return null; + } + final String lcUrl = url.toLowerCase(Locale.US); + if (lcUrl.startsWith("http://") || lcUrl.startsWith("https://")) { + return removeTrailingBracket(url); + } else { + return "http://" + removeTrailingBracket(url); + } + }; + + private static String removeTrailingBracket(final String url) { + int numOpenBrackets = 0; + for (char c : url.toCharArray()) { + if (c == '(') { + ++numOpenBrackets; + } else if (c == ')') { + --numOpenBrackets; + } + } + if (numOpenBrackets != 0 && url.charAt(url.length() - 1) == ')') { + return url.substring(0, url.length() - 1); + } else { + return url; + } + } + + private static final Linkify.MatchFilter WEBURL_MATCH_FILTER = (cs, start, end) -> { + if (start > 0) { + if (cs.charAt(start - 1) == '@' || cs.charAt(start - 1) == '.' + || cs.subSequence(Math.max(0, start - 3), start).equals("://")) { + return false; + } + } + + if (end < cs.length()) { + // Reject strings that were probably matched only because they contain a dot followed by + // by some known TLD (see also comment for WORD_BOUNDARY in Patterns.java) + if (Character.isAlphabetic(cs.charAt(end-1)) && Character.isAlphabetic(cs.charAt(end))) { + return false; + } + } + + return true; + }; + + private static final Linkify.MatchFilter XMPPURI_MATCH_FILTER = (s, start, end) -> { + XmppUri uri = new XmppUri(s.subSequence(start, end).toString()); + return uri.isJidValid(); + }; + + public static void addLinks(Editable body, boolean includeGeo) { + Linkify.addLinks(body, Patterns.XMPP_PATTERN, "xmpp", XMPPURI_MATCH_FILTER, null); + Linkify.addLinks(body, Patterns.AUTOLINK_WEB_URL, "http", WEBURL_MATCH_FILTER, WEBURL_TRANSFORM_FILTER); + if (includeGeo) { + Linkify.addLinks(body, GeoHelper.GEO_URI, "geo"); + } + FixedURLSpan.fix(body); + } +} diff --git a/src/main/res/layout/activity_muc_details.xml b/src/main/res/layout/activity_muc_details.xml index fa2dfc53c..ffce1bcd2 100644 --- a/src/main/res/layout/activity_muc_details.xml +++ b/src/main/res/layout/activity_muc_details.xml @@ -73,6 +73,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentStart="true" + android:autoLink="web" android:layout_below="@+id/muc_title" android:layout_toStartOf="@+id/edit_muc_name_button" android:textAppearance="@style/TextAppearance.Conversations.Subhead"/>