Improve handling of partially bi-directional text.
This commit is contained in:
parent
e504ffa225
commit
8ed7fc894e
7 changed files with 108 additions and 24 deletions
|
@ -18,6 +18,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemov
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GV2AccessLevelUtil;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.StringUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
@ -396,7 +397,7 @@ final class GroupsV2UpdateMessageProducer {
|
|||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
if (change.hasNewTitle()) {
|
||||
String newTitle = change.getNewTitle().getValue();
|
||||
String newTitle = StringUtil.isolateBidi(change.getNewTitle().getValue());
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_name_to_s, newTitle)));
|
||||
} else {
|
||||
|
@ -407,7 +408,7 @@ final class GroupsV2UpdateMessageProducer {
|
|||
|
||||
private void describeUnknownEditorNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.hasNewTitle()) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, change.getNewTitle().getValue())));
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, StringUtil.isolateBidi(change.getNewTitle().getValue()))));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.StringUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Function;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
|
@ -196,8 +197,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||
|
||||
if (profileChangeDetails.hasProfileNameChange()) {
|
||||
String displayName = getIndividualRecipient().getDisplayName(context);
|
||||
String newName = ProfileName.fromSerialized(profileChangeDetails.getProfileNameChange().getNew()).toString();
|
||||
String previousName = ProfileName.fromSerialized(profileChangeDetails.getProfileNameChange().getPrevious()).toString();
|
||||
String newName = StringUtil.isolateBidi(ProfileName.fromSerialized(profileChangeDetails.getProfileNameChange().getNew()).toString());
|
||||
String previousName = StringUtil.isolateBidi(ProfileName.fromSerialized(profileChangeDetails.getProfileNameChange().getPrevious()).toString());
|
||||
|
||||
if (getIndividualRecipient().isSystemContact()) {
|
||||
return context.getString(R.string.MessageRecord_changed_their_profile_name_from_to, displayName, previousName, newName);
|
||||
|
|
|
@ -65,7 +65,7 @@ public class MessageRequestsBottomView extends ConstraintLayout {
|
|||
if (recipient.isGroup()) {
|
||||
question.setText(R.string.MessageRequestBottomView_unblock_this_group_and_share_your_name_and_photo_with_its_members);
|
||||
} else {
|
||||
String name = recipient.getProfileName().isEmpty() ? recipient.getDisplayName(getContext()) : recipient.getProfileName().getGivenName();
|
||||
String name = recipient.getShortDisplayName(getContext());
|
||||
question.setText(HtmlCompat.fromHtml(getContext().getString(R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_wont_receive_any_messages_until_you_unblock_them, HtmlUtil.bold(name)), 0));
|
||||
}
|
||||
setActiveInactiveGroups(blockedButtons, normalButtons);
|
||||
|
@ -77,7 +77,7 @@ public class MessageRequestsBottomView extends ConstraintLayout {
|
|||
question.setText(R.string.MessageRequestBottomView_do_you_want_to_join_this_group_they_wont_know_youve_seen_their_messages_until_you_accept);
|
||||
}
|
||||
} else {
|
||||
String name = recipient.getProfileName().isEmpty() ? recipient.getDisplayName(getContext()) : recipient.getProfileName().getGivenName();
|
||||
String name = recipient.getShortDisplayName(getContext());
|
||||
question.setText(HtmlCompat.fromHtml(getContext().getString(R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_they_wont_know_youve_seen_their_messages_until_you_accept, HtmlUtil.bold(name)), 0));
|
||||
}
|
||||
setActiveInactiveGroups(normalButtons, blockedButtons);
|
||||
|
|
|
@ -26,19 +26,18 @@ import org.thoughtcrime.securesms.contacts.avatars.TransparentContactPhoto;
|
|||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientIdResult;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
@ -400,18 +399,22 @@ public class Recipient {
|
|||
}
|
||||
|
||||
public @NonNull String getDisplayName(@NonNull Context context) {
|
||||
return Util.getFirstNonEmpty(getName(context),
|
||||
getProfileName().toString(),
|
||||
getDisplayUsername(),
|
||||
e164,
|
||||
email,
|
||||
context.getString(R.string.Recipient_unknown));
|
||||
String name = Util.getFirstNonEmpty(getName(context),
|
||||
getProfileName().toString(),
|
||||
getDisplayUsername(),
|
||||
e164,
|
||||
email,
|
||||
context.getString(R.string.Recipient_unknown));
|
||||
|
||||
return StringUtil.isolateBidi(name);
|
||||
}
|
||||
|
||||
public @NonNull String getShortDisplayName(@NonNull Context context) {
|
||||
return Util.getFirstNonEmpty(getName(context),
|
||||
getProfileName().getGivenName(),
|
||||
getDisplayName(context));
|
||||
String name = Util.getFirstNonEmpty(getName(context),
|
||||
getProfileName().getGivenName(),
|
||||
getDisplayName(context));
|
||||
|
||||
return StringUtil.isolateBidi(name);
|
||||
}
|
||||
|
||||
public @NonNull MaterialColor getColor() {
|
||||
|
|
|
@ -125,7 +125,7 @@ public final class GroupUtil {
|
|||
return description.toString();
|
||||
}
|
||||
|
||||
String title = groupContext.getName();
|
||||
String title = StringUtil.isolateBidi(groupContext.getName());
|
||||
|
||||
if (members != null && members.size() > 0) {
|
||||
description.append("\n");
|
||||
|
|
|
@ -2,10 +2,12 @@ package org.thoughtcrime.securesms.util;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.text.BidiFormatter;
|
||||
|
||||
import com.google.android.collect.Sets;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
public final class StringUtil {
|
||||
|
@ -14,6 +16,27 @@ public final class StringUtil {
|
|||
'\u200F', // right-to-left mark
|
||||
'\u2007'); // figure space
|
||||
|
||||
private static final class Bidi {
|
||||
/** Override text direction */
|
||||
private static final Set<Integer> OVERRIDES = Sets.newHashSet("\u202a".codePointAt(0), /* LRE */
|
||||
"\u202b".codePointAt(0), /* RLE */
|
||||
"\u202d".codePointAt(0), /* LRO */
|
||||
"\u202e".codePointAt(0) /* RLO */);
|
||||
|
||||
/** Set direction and isolate surrounding text */
|
||||
private static final Set<Integer> ISOLATES = Sets.newHashSet("\u2066".codePointAt(0), /* LRI */
|
||||
"\u2067".codePointAt(0), /* RLI */
|
||||
"\u2068".codePointAt(0) /* FSI */);
|
||||
/** Closes things in {@link #OVERRIDES} */
|
||||
private static final int PDF = "\u202c".codePointAt(0);
|
||||
|
||||
/** Closes things in {@link #ISOLATES} */
|
||||
private static final int PDI = "\u2069".codePointAt(0);
|
||||
|
||||
/** Auto-detecting isolate */
|
||||
private static final int FSI = "\u2068".codePointAt(0);
|
||||
}
|
||||
|
||||
private StringUtil() {
|
||||
}
|
||||
|
||||
|
@ -99,4 +122,58 @@ public final class StringUtil {
|
|||
public static @NonNull String codePointToString(int codePoint) {
|
||||
return new String(Character.toChars(codePoint));
|
||||
}
|
||||
|
||||
/**
|
||||
* Isolates bi-directional text from influencing surrounding text. You should use this whenever
|
||||
* you're injecting user-generated text into a larger string.
|
||||
*
|
||||
* You'd think we'd be able to trust {@link BidiFormatter}, but unfortunately it just misses some
|
||||
* corner cases, so here we are.
|
||||
*
|
||||
* The general idea is just to balance out the opening and closing codepoints, and then wrap the
|
||||
* whole thing in FSI/PDI to isolate it.
|
||||
*
|
||||
* For more details, see:
|
||||
* https://www.w3.org/International/questions/qa-bidi-unicode-controls
|
||||
*/
|
||||
public static @NonNull String isolateBidi(@NonNull String text) {
|
||||
int overrideCount = 0;
|
||||
int overrideCloseCount = 0;
|
||||
int isolateCount = 0;
|
||||
int isolateCloseCount = 0;
|
||||
|
||||
for (int i = 0, len = text.codePointCount(0, text.length()); i < len; i++) {
|
||||
int codePoint = text.codePointAt(i);
|
||||
|
||||
if (Bidi.OVERRIDES.contains(codePoint)) {
|
||||
overrideCount++;
|
||||
} else if (codePoint == Bidi.PDF) {
|
||||
overrideCloseCount++;
|
||||
} else if (Bidi.ISOLATES.contains(codePoint)) {
|
||||
isolateCount++;
|
||||
} else if (codePoint == Bidi.PDI) {
|
||||
isolateCloseCount++;
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder suffix = new StringBuilder();
|
||||
|
||||
while (overrideCount > overrideCloseCount) {
|
||||
suffix.appendCodePoint(Bidi.PDF);
|
||||
overrideCloseCount++;
|
||||
}
|
||||
|
||||
while (isolateCount > isolateCloseCount) {
|
||||
suffix.appendCodePoint(Bidi.FSI);
|
||||
isolateCloseCount++;
|
||||
}
|
||||
|
||||
StringBuilder out = new StringBuilder();
|
||||
|
||||
return out.appendCodePoint(Bidi.FSI)
|
||||
.append(text)
|
||||
.append(suffix)
|
||||
.appendCodePoint(Bidi.PDI)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemov
|
|||
import org.signal.storageservice.protos.groups.local.DecryptedString;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
|
||||
import org.thoughtcrime.securesms.testutil.MainThreadUtil;
|
||||
import org.thoughtcrime.securesms.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
|
@ -43,6 +44,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
|
|||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.thoughtcrime.securesms.util.StringUtil.isolateBidi;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE, application = Application.class)
|
||||
|
@ -603,7 +605,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.title("New title")
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("Alice changed the group name to \"New title\".")));
|
||||
assertThat(describeChange(change), is(singletonList("Alice changed the group name to \"" + isolateBidi("New title") + "\".")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -612,7 +614,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.title("Title 2")
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("You changed the group name to \"Title 2\".")));
|
||||
assertThat(describeChange(change), is(singletonList("You changed the group name to \"" + isolateBidi("Title 2") + "\".")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -621,7 +623,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
.title("Title 3")
|
||||
.build();
|
||||
|
||||
assertThat(describeChange(change), is(singletonList("The group name has changed to \"Title 3\".")));
|
||||
assertThat(describeChange(change), is(singletonList("The group name has changed to \"" + isolateBidi("Title 3") + "\".")));
|
||||
}
|
||||
|
||||
// Avatar change
|
||||
|
@ -762,7 +764,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
|
||||
assertThat(describeChange(change), is(Arrays.asList(
|
||||
"Alice added Bob.",
|
||||
"Alice changed the group name to \"Title\".",
|
||||
"Alice changed the group name to \"" + isolateBidi("Title") + "\".",
|
||||
"Alice set the disappearing message timer to 5 minutes.",
|
||||
"Alice changed who can edit group membership to \"All members\".")));
|
||||
}
|
||||
|
@ -803,7 +805,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
|
||||
assertThat(describeChange(change), is(Arrays.asList(
|
||||
"Bob joined the group.",
|
||||
"The group name has changed to \"Title 2\".",
|
||||
"The group name has changed to \"" + isolateBidi("Title 2") + "\".",
|
||||
"The group avatar has been changed.",
|
||||
"The disappearing message timer has been set to 10 minutes.",
|
||||
"Who can edit group membership has been changed to \"All members\".")));
|
||||
|
@ -821,7 +823,7 @@ public final class GroupsV2UpdateMessageProducerTest {
|
|||
assertThat(describeChange(change), is(Arrays.asList(
|
||||
"Alice joined the group.",
|
||||
"Alice is now an admin.",
|
||||
"The group name has changed to \"Updated title\".",
|
||||
"The group name has changed to \"" + isolateBidi("Updated title") + "\".",
|
||||
"Alice is no longer in the group.")));
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue