Perform bidirectionality isolation manually instead of relying on `BidiFormatter` (#1976)
* Perform manual isolation of display names etc. instead of relying on BidiFormatter. Fixes #1921 * Make follow request notification header formatting more like other notifications
This commit is contained in:
parent
d7e47caef3
commit
2170e6b0fa
|
@ -1,13 +1,19 @@
|
||||||
package com.keylesspalace.tusky.adapter
|
package com.keylesspalace.tusky.adapter
|
||||||
|
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.style.StyleSpan
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.text.BidiFormatter
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.emojify
|
||||||
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
import com.keylesspalace.tusky.util.unicodeWrap
|
||||||
|
import com.keylesspalace.tusky.util.visible
|
||||||
import kotlinx.android.synthetic.main.item_follow_request_notification.view.*
|
import kotlinx.android.synthetic.main.item_follow_request_notification.view.*
|
||||||
|
|
||||||
internal class FollowRequestViewHolder(itemView: View, private val showHeader: Boolean) : RecyclerView.ViewHolder(itemView) {
|
internal class FollowRequestViewHolder(itemView: View, private val showHeader: Boolean) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
@ -15,13 +21,16 @@ internal class FollowRequestViewHolder(itemView: View, private val showHeader: B
|
||||||
private val animateAvatar: Boolean = PreferenceManager.getDefaultSharedPreferences(itemView.context)
|
private val animateAvatar: Boolean = PreferenceManager.getDefaultSharedPreferences(itemView.context)
|
||||||
.getBoolean("animateGifAvatars", false)
|
.getBoolean("animateGifAvatars", false)
|
||||||
|
|
||||||
fun setupWithAccount(account: Account, formatter: BidiFormatter?) {
|
fun setupWithAccount(account: Account) {
|
||||||
id = account.id
|
id = account.id
|
||||||
val wrappedName = formatter?.unicodeWrap(account.name) ?: account.name
|
val wrappedName = account.name.unicodeWrap()
|
||||||
val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView)
|
val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView)
|
||||||
itemView.displayNameTextView.text = emojifiedName
|
itemView.displayNameTextView.text = emojifiedName
|
||||||
if (showHeader) {
|
if (showHeader) {
|
||||||
itemView.notificationTextView?.text = itemView.context.getString(R.string.notification_follow_request_format, emojifiedName)
|
val wholeMessage: String = itemView.context.getString(R.string.notification_follow_request_format, wrappedName)
|
||||||
|
itemView.notificationTextView?.text = SpannableStringBuilder(wholeMessage).apply {
|
||||||
|
setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
}.emojify(account.emojis, itemView)
|
||||||
}
|
}
|
||||||
itemView.notificationTextView?.visible(showHeader)
|
itemView.notificationTextView?.visible(showHeader)
|
||||||
val format = itemView.context.getString(R.string.status_username_format)
|
val format = itemView.context.getString(R.string.status_username_format)
|
||||||
|
|
|
@ -53,7 +53,7 @@ public class FollowRequestsAdapter extends AccountAdapter {
|
||||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||||
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
||||||
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
|
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
|
||||||
holder.setupWithAccount(accountList.get(position), null);
|
holder.setupWithAccount(accountList.get(position));
|
||||||
holder.setupActionListener(accountActionListener);
|
holder.setupActionListener(accountActionListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,6 @@ import android.widget.TextView;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.text.BidiFormatter;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
|
@ -51,6 +50,7 @@ import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||||
import com.keylesspalace.tusky.util.LinkHelper;
|
import com.keylesspalace.tusky.util.LinkHelper;
|
||||||
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
|
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
|
||||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||||
|
import com.keylesspalace.tusky.util.StringUtils;
|
||||||
import com.keylesspalace.tusky.util.TimestampUtils;
|
import com.keylesspalace.tusky.util.TimestampUtils;
|
||||||
import com.keylesspalace.tusky.viewdata.NotificationViewData;
|
import com.keylesspalace.tusky.viewdata.NotificationViewData;
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
@ -86,7 +86,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
private StatusActionListener statusListener;
|
private StatusActionListener statusListener;
|
||||||
private NotificationActionListener notificationActionListener;
|
private NotificationActionListener notificationActionListener;
|
||||||
private AccountActionListener accountActionListener;
|
private AccountActionListener accountActionListener;
|
||||||
private BidiFormatter bidiFormatter;
|
|
||||||
private AdapterDataSource<NotificationViewData> dataSource;
|
private AdapterDataSource<NotificationViewData> dataSource;
|
||||||
|
|
||||||
public NotificationsAdapter(String accountId,
|
public NotificationsAdapter(String accountId,
|
||||||
|
@ -102,7 +101,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
this.statusListener = statusListener;
|
this.statusListener = statusListener;
|
||||||
this.notificationActionListener = notificationActionListener;
|
this.notificationActionListener = notificationActionListener;
|
||||||
this.accountActionListener = accountActionListener;
|
this.accountActionListener = accountActionListener;
|
||||||
bidiFormatter = BidiFormatter.getInstance();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -204,7 +202,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
concreteNotificaton.getAccount().getAvatar());
|
concreteNotificaton.getAccount().getAvatar());
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.setMessage(concreteNotificaton, statusListener, bidiFormatter);
|
holder.setMessage(concreteNotificaton, statusListener);
|
||||||
holder.setupButtons(notificationActionListener,
|
holder.setupButtons(notificationActionListener,
|
||||||
concreteNotificaton.getAccount().getId(),
|
concreteNotificaton.getAccount().getId(),
|
||||||
concreteNotificaton.getId());
|
concreteNotificaton.getId());
|
||||||
|
@ -221,7 +219,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
case VIEW_TYPE_FOLLOW: {
|
case VIEW_TYPE_FOLLOW: {
|
||||||
if (payloadForHolder == null) {
|
if (payloadForHolder == null) {
|
||||||
FollowViewHolder holder = (FollowViewHolder) viewHolder;
|
FollowViewHolder holder = (FollowViewHolder) viewHolder;
|
||||||
holder.setMessage(concreteNotificaton.getAccount(), bidiFormatter);
|
holder.setMessage(concreteNotificaton.getAccount());
|
||||||
holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId());
|
holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -229,7 +227,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
case VIEW_TYPE_FOLLOW_REQUEST: {
|
case VIEW_TYPE_FOLLOW_REQUEST: {
|
||||||
if (payloadForHolder == null) {
|
if (payloadForHolder == null) {
|
||||||
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
|
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
|
||||||
holder.setupWithAccount(concreteNotificaton.getAccount(), bidiFormatter);
|
holder.setupWithAccount(concreteNotificaton.getAccount());
|
||||||
holder.setupActionListener(accountActionListener);
|
holder.setupActionListener(accountActionListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -325,11 +323,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
this.statusDisplayOptions = statusDisplayOptions;
|
this.statusDisplayOptions = statusDisplayOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMessage(Account account, BidiFormatter bidiFormatter) {
|
void setMessage(Account account) {
|
||||||
Context context = message.getContext();
|
Context context = message.getContext();
|
||||||
|
|
||||||
String format = context.getString(R.string.notification_follow_format);
|
String format = context.getString(R.string.notification_follow_format);
|
||||||
String wrappedDisplayName = bidiFormatter.unicodeWrap(account.getName());
|
String wrappedDisplayName = StringUtils.unicodeWrap(account.getName());
|
||||||
String wholeMessage = String.format(format, wrappedDisplayName);
|
String wholeMessage = String.format(format, wrappedDisplayName);
|
||||||
CharSequence emojifiedMessage = CustomEmojiHelper.emojify(wholeMessage, account.getEmojis(), message);
|
CharSequence emojifiedMessage = CustomEmojiHelper.emojify(wholeMessage, account.getEmojis(), message);
|
||||||
message.setText(emojifiedMessage);
|
message.setText(emojifiedMessage);
|
||||||
|
@ -459,10 +457,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener, BidiFormatter bidiFormatter) {
|
void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener) {
|
||||||
this.statusViewData = notificationViewData.getStatusViewData();
|
this.statusViewData = notificationViewData.getStatusViewData();
|
||||||
|
|
||||||
String displayName = bidiFormatter.unicodeWrap(notificationViewData.getAccount().getName());
|
String displayName = StringUtils.unicodeWrap(notificationViewData.getAccount().getName());
|
||||||
Notification.Type type = notificationViewData.getType();
|
Notification.Type type = notificationViewData.getType();
|
||||||
|
|
||||||
Context context = message.getContext();
|
Context context = message.getContext();
|
||||||
|
|
|
@ -37,7 +37,6 @@ import androidx.core.app.NotificationManagerCompat;
|
||||||
import androidx.core.app.RemoteInput;
|
import androidx.core.app.RemoteInput;
|
||||||
import androidx.core.app.TaskStackBuilder;
|
import androidx.core.app.TaskStackBuilder;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.text.BidiFormatter;
|
|
||||||
import androidx.work.Constraints;
|
import androidx.work.Constraints;
|
||||||
import androidx.work.NetworkType;
|
import androidx.work.NetworkType;
|
||||||
import androidx.work.PeriodicWorkRequest;
|
import androidx.work.PeriodicWorkRequest;
|
||||||
|
@ -58,6 +57,7 @@ import com.keylesspalace.tusky.entity.PollOption;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver;
|
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver;
|
||||||
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver;
|
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver;
|
||||||
|
import com.keylesspalace.tusky.util.StringUtils;
|
||||||
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
|
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
|
@ -145,7 +145,6 @@ public class NotificationHelper {
|
||||||
|
|
||||||
String rawCurrentNotifications = account.getActiveNotifications();
|
String rawCurrentNotifications = account.getActiveNotifications();
|
||||||
JSONArray currentNotifications;
|
JSONArray currentNotifications;
|
||||||
BidiFormatter bidiFormatter = BidiFormatter.getInstance();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
currentNotifications = new JSONArray(rawCurrentNotifications);
|
currentNotifications = new JSONArray(rawCurrentNotifications);
|
||||||
|
@ -174,7 +173,7 @@ public class NotificationHelper {
|
||||||
|
|
||||||
notificationId++;
|
notificationId++;
|
||||||
|
|
||||||
builder.setContentTitle(titleForType(context, body, bidiFormatter, account))
|
builder.setContentTitle(titleForType(context, body, account))
|
||||||
.setContentText(bodyForType(body, context));
|
.setContentText(bodyForType(body, context));
|
||||||
|
|
||||||
if (body.getType() == Notification.Type.MENTION || body.getType() == Notification.Type.POLL) {
|
if (body.getType() == Notification.Type.MENTION || body.getType() == Notification.Type.POLL) {
|
||||||
|
@ -243,7 +242,7 @@ public class NotificationHelper {
|
||||||
if (currentNotifications.length() != 1) {
|
if (currentNotifications.length() != 1) {
|
||||||
try {
|
try {
|
||||||
String title = context.getString(R.string.notification_title_summary, currentNotifications.length());
|
String title = context.getString(R.string.notification_title_summary, currentNotifications.length());
|
||||||
String text = joinNames(context, currentNotifications, bidiFormatter);
|
String text = joinNames(context, currentNotifications);
|
||||||
summaryBuilder.setContentTitle(title)
|
summaryBuilder.setContentTitle(title)
|
||||||
.setContentText(text);
|
.setContentText(text);
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
|
@ -573,36 +572,36 @@ public class NotificationHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String wrapItemAt(JSONArray array, int index, BidiFormatter bidiFormatter) throws JSONException {
|
private static String wrapItemAt(JSONArray array, int index) throws JSONException {
|
||||||
return bidiFormatter.unicodeWrap(array.get(index).toString());
|
return StringUtils.unicodeWrap(array.get(index).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static String joinNames(Context context, JSONArray array, BidiFormatter bidiFormatter) throws JSONException {
|
private static String joinNames(Context context, JSONArray array) throws JSONException {
|
||||||
if (array.length() > 3) {
|
if (array.length() > 3) {
|
||||||
int length = array.length();
|
int length = array.length();
|
||||||
return String.format(context.getString(R.string.notification_summary_large),
|
return String.format(context.getString(R.string.notification_summary_large),
|
||||||
wrapItemAt(array, length - 1, bidiFormatter),
|
wrapItemAt(array, length - 1),
|
||||||
wrapItemAt(array, length - 2, bidiFormatter),
|
wrapItemAt(array, length - 2),
|
||||||
wrapItemAt(array, length - 3, bidiFormatter),
|
wrapItemAt(array, length - 3),
|
||||||
length - 3);
|
length - 3);
|
||||||
} else if (array.length() == 3) {
|
} else if (array.length() == 3) {
|
||||||
return String.format(context.getString(R.string.notification_summary_medium),
|
return String.format(context.getString(R.string.notification_summary_medium),
|
||||||
wrapItemAt(array, 2, bidiFormatter),
|
wrapItemAt(array, 2),
|
||||||
wrapItemAt(array, 1, bidiFormatter),
|
wrapItemAt(array, 1),
|
||||||
wrapItemAt(array, 0, bidiFormatter));
|
wrapItemAt(array, 0));
|
||||||
} else if (array.length() == 2) {
|
} else if (array.length() == 2) {
|
||||||
return String.format(context.getString(R.string.notification_summary_small),
|
return String.format(context.getString(R.string.notification_summary_small),
|
||||||
wrapItemAt(array, 1, bidiFormatter),
|
wrapItemAt(array, 1),
|
||||||
wrapItemAt(array, 0, bidiFormatter));
|
wrapItemAt(array, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static String titleForType(Context context, Notification notification, BidiFormatter bidiFormatter, AccountEntity account) {
|
private static String titleForType(Context context, Notification notification, AccountEntity account) {
|
||||||
String accountName = bidiFormatter.unicodeWrap(notification.getAccount().getName());
|
String accountName = StringUtils.unicodeWrap(notification.getAccount().getName());
|
||||||
switch (notification.getType()) {
|
switch (notification.getType()) {
|
||||||
case MENTION:
|
case MENTION:
|
||||||
return String.format(context.getString(R.string.notification_mention_format),
|
return String.format(context.getString(R.string.notification_mention_format),
|
||||||
|
|
|
@ -80,3 +80,12 @@ fun Spanned.trimTrailingWhitespace(): Spanned {
|
||||||
} while (i >= 0 && get(i).isWhitespace())
|
} while (i >= 0 && get(i).isWhitespace())
|
||||||
return subSequence(0, i + 1) as Spanned
|
return subSequence(0, i + 1) as Spanned
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BidiFormatter.unicodeWrap is insufficient in some cases (see #1921)
|
||||||
|
* So we force isolation manually
|
||||||
|
* https://unicode.org/reports/tr9/#Explicit_Directional_Isolates
|
||||||
|
*/
|
||||||
|
fun CharSequence.unicodeWrap(): String {
|
||||||
|
return "\u2068${this}\u2069"
|
||||||
|
}
|
|
@ -20,7 +20,7 @@
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:paddingStart="28dp"
|
android:paddingStart="28dp"
|
||||||
android:textColor="?android:textColorTertiary"
|
android:textColor="?android:textColorSecondary"
|
||||||
android:textSize="?attr/status_text_medium"
|
android:textSize="?attr/status_text_medium"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
|
Loading…
Reference in New Issue