From cfc7abd31af7a9e456c090ae66936aaa7d7d536a Mon Sep 17 00:00:00 2001 From: Vavassor Date: Sat, 1 Jul 2017 20:32:35 -0400 Subject: [PATCH] Changes mention and tag highlighting in the composer to use Mastodon's regex. Closes #145 Also, does some haphazard cleanup. --- .../keylesspalace/tusky/AccountActivity.java | 1 - .../tusky/EditProfileActivity.java | 3 +- .../tusky/json/SpannedTypeAdapter.java | 11 ++- .../tusky/network/MastodonApi.java | 5 +- .../keylesspalace/tusky/util/LinkHelper.java | 4 +- .../tusky/util/NotificationMaker.java | 67 +++++++++++-------- .../keylesspalace/tusky/util/SpanUtils.java | 52 +++++++------- 7 files changed, 83 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java index 70884ae3..bc3a0233 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java @@ -244,7 +244,6 @@ public class AccountActivity extends BaseActivity { String subtitle = String.format(getString(R.string.status_username_format), account.username); getSupportActionBar().setSubtitle(subtitle); - } boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(this) diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.java b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.java index 1961606c..455dcc64 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.java @@ -170,7 +170,8 @@ public class EditProfileActivity extends BaseActivity { Account me = response.body(); priorDisplayName = me.getDisplayName(); priorNote = me.note.toString(); - CircularImageView avatar = (CircularImageView) findViewById(R.id.edit_profile_avatar_preview); + CircularImageView avatar = + (CircularImageView) findViewById(R.id.edit_profile_avatar_preview); ImageView header = (ImageView) findViewById(R.id.edit_profile_header_preview); displayNameEditText.setText(priorDisplayName); diff --git a/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.java b/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.java index 305adba4..ef617368 100644 --- a/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.java @@ -16,6 +16,7 @@ package com.keylesspalace.tusky.json; import android.text.Spanned; +import android.text.SpannedString; import com.emojione.Emojione; import com.google.gson.JsonDeserializationContext; @@ -28,7 +29,13 @@ import java.lang.reflect.Type; public class SpannedTypeAdapter implements JsonDeserializer { @Override - public Spanned deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - return HtmlUtils.fromHtml(Emojione.shortnameToUnicode(json.getAsString(), false)); + public Spanned deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + String string = json.getAsString(); + if (string != null) { + return HtmlUtils.fromHtml(Emojione.shortnameToUnicode(string, false)); + } else { + return new SpannedString(""); + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java index 2fd66184..8a039fa8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java @@ -190,7 +190,10 @@ public interface MastodonApi { @FormUrlEncoded @POST("api/v1/reports") - Call report(@Field("account_id") String accountId, @Field("status_ids[]") List statusIds, @Field("comment") String comment); + Call report( + @Field("account_id") String accountId, + @Field("status_ids[]") List statusIds, + @Field("comment") String comment); @GET("api/v1/search") Call search(@Query("q") String q, @Query("resolve") Boolean resolve); diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java index 55ff6b7c..b1f31f6a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java @@ -47,8 +47,8 @@ public class LinkHelper { } public static void setClickableText(TextView view, Spanned content, - @Nullable Status.Mention[] mentions, boolean useCustomTabs, - final LinkListener listener) { + @Nullable Status.Mention[] mentions, boolean useCustomTabs, + final LinkListener listener) { SpannableStringBuilder builder = new SpannableStringBuilder(content); URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class); for (URLSpan span : urlSpans) { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/NotificationMaker.java b/app/src/main/java/com/keylesspalace/tusky/util/NotificationMaker.java index 156a9542..e347a5ea 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/NotificationMaker.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/NotificationMaker.java @@ -41,7 +41,7 @@ import com.squareup.picasso.Target; import org.json.JSONArray; import org.json.JSONException; -public class NotificationMaker { +class NotificationMaker { public static final String TAG = "NotificationMaker"; @@ -89,10 +89,12 @@ public class NotificationMaker { TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); stackBuilder.addParentStack(MainActivity.class); stackBuilder.addNextIntent(resultIntent); - PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, + PendingIntent.FLAG_UPDATE_CURRENT); Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class); - PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, 0, deleteIntent, PendingIntent.FLAG_CANCEL_CURRENT); + PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, 0, deleteIntent, + PendingIntent.FLAG_CANCEL_CURRENT); final NotificationCompat.Builder builder = new NotificationCompat.Builder(context) .setSmallIcon(R.drawable.ic_notify) @@ -104,15 +106,16 @@ public class NotificationMaker { builder.setContentTitle(titleForType(context, body)) .setContentText(truncateWithEllipses(bodyForType(body), 40)); - Target mTarget = new Target() { + Target target = new Target() { @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { builder.setLargeIcon(bitmap); setupPreferences(preferences, builder); - ((NotificationManager) (context.getSystemService(Context.NOTIFICATION_SERVICE))) - .notify(notifyId, builder.build()); + NotificationManager notificationManager = (NotificationManager) + context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(notifyId, builder.build()); } @Override @@ -126,12 +129,15 @@ public class NotificationMaker { .load(body.account.avatar) .placeholder(R.drawable.avatar_default) .transform(new RoundedTransformation(7, 0)) - .into(mTarget); + .into(target); } else { setupPreferences(preferences, builder); try { - builder.setContentTitle(String.format(context.getString(R.string.notification_title_summary), currentNotifications.length())) - .setContentText(truncateWithEllipses(joinNames(context, currentNotifications), 40)); + String format = context.getString(R.string.notification_title_summary); + String title = String.format(format, currentNotifications.length()); + String text = truncateWithEllipses(joinNames(context, currentNotifications), 40); + builder.setContentTitle(title) + .setContentText(text); } catch (JSONException e) { Log.d(TAG, Log.getStackTraceString(e)); } @@ -142,26 +148,23 @@ public class NotificationMaker { builder.setCategory(android.app.Notification.CATEGORY_SOCIAL); } - ((NotificationManager) (context.getSystemService(Context.NOTIFICATION_SERVICE))) - .notify(notifyId, builder.build()); + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(notifyId, builder.build()); } private static boolean filterNotification(SharedPreferences preferences, - Notification notification) { + Notification notification) { switch (notification.type) { default: - case MENTION: { + case MENTION: return preferences.getBoolean("notificationFilterMentions", true); - } - case FOLLOW: { + case FOLLOW: return preferences.getBoolean("notificationFilterFollows", true); - } - case REBLOG: { + case REBLOG: return preferences.getBoolean("notificationFilterReblogs", true); - } - case FAVOURITE: { + case FAVOURITE: return preferences.getBoolean("notificationFilterFavourites", true); - } } } @@ -174,7 +177,7 @@ public class NotificationMaker { } private static void setupPreferences(SharedPreferences preferences, - NotificationCompat.Builder builder) { + NotificationCompat.Builder builder) { if (preferences.getBoolean("notificationAlertSound", true)) { builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI); } @@ -191,11 +194,14 @@ public class NotificationMaker { @Nullable private static String joinNames(Context context, JSONArray array) throws JSONException { if (array.length() > 3) { - return String.format(context.getString(R.string.notification_summary_large), array.get(0), array.get(1), array.get(2), array.length() - 3); + return String.format(context.getString(R.string.notification_summary_large), + array.get(0), array.get(1), array.get(2), array.length() - 3); } else if (array.length() == 3) { - return String.format(context.getString(R.string.notification_summary_medium), array.get(0), array.get(1), array.get(2)); + return String.format(context.getString(R.string.notification_summary_medium), + array.get(0), array.get(1), array.get(2)); } else if (array.length() == 2) { - return String.format(context.getString(R.string.notification_summary_small), array.get(0), array.get(1)); + return String.format(context.getString(R.string.notification_summary_small), + array.get(0), array.get(1)); } return null; @@ -205,13 +211,17 @@ public class NotificationMaker { private static String titleForType(Context context, Notification notification) { switch (notification.type) { case MENTION: - return String.format(context.getString(R.string.notification_mention_format), notification.account.getDisplayName()); + return String.format(context.getString(R.string.notification_mention_format), + notification.account.getDisplayName()); case FOLLOW: - return String.format(context.getString(R.string.notification_follow_format), notification.account.getDisplayName()); + return String.format(context.getString(R.string.notification_follow_format), + notification.account.getDisplayName()); case FAVOURITE: - return String.format(context.getString(R.string.notification_favourite_format), notification.account.getDisplayName()); + return String.format(context.getString(R.string.notification_favourite_format), + notification.account.getDisplayName()); case REBLOG: - return String.format(context.getString(R.string.notification_reblog_format), notification.account.getDisplayName()); + return String.format(context.getString(R.string.notification_reblog_format), + notification.account.getDisplayName()); } return null; } @@ -226,7 +236,6 @@ public class NotificationMaker { case REBLOG: return notification.status.content.toString(); } - return null; } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.java index 81a82d2b..f2270117 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.java @@ -19,7 +19,17 @@ import android.text.Spannable; import android.text.Spanned; import android.text.style.ForegroundColorSpan; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + public class SpanUtils { + private static final String TAG_REGEX = "(?:^|[^/)\\w])#([\\w_]*[\\p{Alpha}_][\\w_]*)"; + private static Pattern TAG_PATTERN = Pattern.compile(TAG_REGEX, Pattern.CASE_INSENSITIVE); + private static final String MENTION_REGEX = + "(?:^|[^/[:word:]])@([a-z0-9_]+(?:@[a-z0-9\\.\\-]+[a-z0-9]+)?)"; + private static Pattern MENTION_PATTERN = + Pattern.compile(MENTION_REGEX, Pattern.CASE_INSENSITIVE); + private static class FindCharsResult { int charIndex; int stringIndex; @@ -63,35 +73,29 @@ public class SpanUtils { } private static int findEndOfHashtag(String string, int fromIndex) { - final int length = string.length(); - for (int i = fromIndex + 1; i < length;) { - int codepoint = string.codePointAt(i); - if (Character.isWhitespace(codepoint)) { - return i; - } else if (codepoint == '#') { - return -1; - } - i += Character.charCount(codepoint); + Matcher matcher = TAG_PATTERN.matcher(string); + if (fromIndex >= 1) { + fromIndex--; + } + boolean found = matcher.find(fromIndex); + if (found) { + return matcher.end(); + } else { + return -1; } - return length; } private static int findEndOfMention(String string, int fromIndex) { - int atCount = 0; - final int length = string.length(); - for (int i = fromIndex + 1; i < length;) { - int codepoint = string.codePointAt(i); - if (Character.isWhitespace(codepoint)) { - return i; - } else if (codepoint == '@') { - atCount += 1; - if (atCount >= 2) { - return -1; - } - } - i += Character.charCount(codepoint); + Matcher matcher = MENTION_PATTERN.matcher(string); + if (fromIndex >= 1) { + fromIndex--; + } + boolean found = matcher.find(fromIndex); + if (found) { + return matcher.end(); + } else { + return -1; } - return length; } public static void highlightSpans(Spannable text, int colour) {