From 5901967eee8158146c00aee43bcf61c174b702f0 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Wed, 9 Oct 2019 16:41:14 +1100 Subject: [PATCH 01/15] Implement mention rendering --- res/values/colors.xml | 1 + .../conversation/ConversationItem.java | 50 ++++++++++++++++--- .../securesms/loki/SeedActivity.kt | 24 ++++----- 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/res/values/colors.xml b/res/values/colors.xml index e9e32991cc..593a27dbd2 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -2,6 +2,7 @@ #78be20 + #419B41 #0a0a0a diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java index 798d843ab0..236966da66 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -42,6 +42,7 @@ import android.text.style.ForegroundColorSpan; import android.text.style.URLSpan; import android.text.util.Linkify; import android.util.AttributeSet; +import android.util.Range; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; @@ -114,11 +115,14 @@ import org.thoughtcrime.securesms.util.views.Stub; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import network.loki.messenger.R; @@ -258,7 +262,7 @@ public class ConversationItem extends LinearLayout setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread); setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, conversationRecipient, groupThread); setInteractionState(messageRecord, pulseHighlight); - setBodyText(messageRecord, searchQuery); + setBodyText(messageRecord, searchQuery, groupThread); setBubbleState(messageRecord); setStatusIcons(messageRecord); setContactPhoto(recipient); @@ -464,7 +468,7 @@ public class ConversationItem extends LinearLayout !StickerUrl.isValidShareLink(linkPreview.getUrl()); } - private void setBodyText(MessageRecord messageRecord, @Nullable String searchQuery) { + private void setBodyText(MessageRecord messageRecord, @Nullable String searchQuery, boolean isGroupThread) { bodyText.setClickable(false); bodyText.setFocusable(false); bodyText.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(context)); @@ -472,9 +476,9 @@ public class ConversationItem extends LinearLayout if (isCaptionlessMms(messageRecord)) { bodyText.setVisibility(View.GONE); } else { - Spannable styledText = linkifyMessageBody(messageRecord.getDisplayBody(getContext()), batchSelected.isEmpty()); - styledText = SearchUtil.getHighlightedSpan(locale, () -> new BackgroundColorSpan(Color.YELLOW), styledText, searchQuery); - styledText = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), styledText, searchQuery); + Spannable text = linkifyMessageBody(highlightMentions(messageRecord.getDisplayBody(context), isGroupThread), batchSelected.isEmpty()); + text = SearchUtil.getHighlightedSpan(locale, () -> new BackgroundColorSpan(Color.YELLOW), text, searchQuery); + text = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), text, searchQuery); if (hasExtraText(messageRecord)) { bodyText.setOverflowText(getLongMessageSpan(messageRecord)); @@ -482,7 +486,7 @@ public class ConversationItem extends LinearLayout bodyText.setOverflowText(null); } - bodyText.setText(styledText); + bodyText.setText(text); bodyText.setVisibility(View.VISIBLE); } } @@ -772,6 +776,40 @@ public class ConversationItem extends LinearLayout return messageBody; } + private SpannableString highlightMentions(CharSequence text, boolean isGroupThread) { + Pattern pattern = Pattern.compile("@\\w*"); + Matcher matcher = pattern.matcher(text); + ArrayList> mentions = new ArrayList<>(); + if (matcher.find() && isGroupThread) { + while (true) { + CharSequence userID = text.subSequence(matcher.start() + 1, matcher.end()); // +1 to get rid of the @ + Integer matchEnd; + String userDisplayName; + if (userID.equals(TextSecurePreferences.getLocalNumber(context))) { + userDisplayName = TextSecurePreferences.getProfileName(context); + } else { + String publicChatID = LokiGroupChatAPI.getPublicChatServer() + "." + LokiGroupChatAPI.getPublicChatServerID(); + userDisplayName = DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChatID, userID.toString()); + } + if (userDisplayName != null) { + text = text.subSequence(0, matcher.start()) + "@" + userDisplayName + text.subSequence(matcher.end(), Math.max(text.length(), matcher.end())); + matchEnd = matcher.start() + 1 + userDisplayName.length(); + mentions.add(Range.create(matcher.start(), matchEnd)); + } else { + matchEnd = matcher.end(); + } + matcher = pattern.matcher(text.subSequence(matchEnd, Math.max(text.length(), matchEnd))); + if (!matcher.find()) { break; } + } + } + SpannableString result = new SpannableString(text); + for (Range range : mentions) { + int highlightColor = (messageRecord.isOutgoing()) ? getResources().getColor(R.color.loki_dark_green) : getResources().getColor(R.color.loki_green); + result.setSpan(new BackgroundColorSpan(highlightColor), range.getLower(), range.getUpper(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + return result; + } + private void setStatusIcons(MessageRecord messageRecord) { bodyText.setCompoundDrawablesWithIntrinsicBounds(0, 0, messageRecord.isKeyExchange() ? R.drawable.ic_menu_login : 0, 0); diff --git a/src/org/thoughtcrime/securesms/loki/SeedActivity.kt b/src/org/thoughtcrime/securesms/loki/SeedActivity.kt index 8da00fe60c..a98b6c5a46 100644 --- a/src/org/thoughtcrime/securesms/loki/SeedActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/SeedActivity.kt @@ -9,9 +9,6 @@ import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.Toast import kotlinx.android.synthetic.main.activity_seed.* -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import network.loki.messenger.R import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity @@ -180,13 +177,12 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate { IdentityKeyUtil.generateIdentityKeyPair(this, seed) } val keyPair = IdentityKeyUtil.getIdentityKeyPair(this) - val publicKey = keyPair.publicKey - val hexEncodedPublicKey = keyPair.hexEncodedPublicKey + val userHexEncodedPublicKey = keyPair.hexEncodedPublicKey val registrationID = KeyHelper.generateRegistrationId(false) TextSecurePreferences.setLocalRegistrationId(this, registrationID) - DatabaseFactory.getIdentityDatabase(this).saveIdentity(Address.fromSerialized(hexEncodedPublicKey), publicKey, + DatabaseFactory.getIdentityDatabase(this).saveIdentity(Address.fromSerialized(userHexEncodedPublicKey), keyPair.publicKey, IdentityDatabase.VerifiedStatus.VERIFIED, true, System.currentTimeMillis(), true) - TextSecurePreferences.setLocalNumber(this, hexEncodedPublicKey) + TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey) when (mode) { Mode.Register -> Analytics.shared.track("Seed Created") Mode.Restore -> Analytics.shared.track("Seed Restored") @@ -195,22 +191,20 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate { if (mode == Mode.Link) { TextSecurePreferences.setHasSeenWelcomeScreen(this, true) TextSecurePreferences.setPromptedPushRegistration(this, true) - val primaryDevicePublicKey = publicKeyEditText.text.trim().toString() - val authorisation = PairingAuthorisation(primaryDevicePublicKey, hexEncodedPublicKey).sign(PairingAuthorisation.Type.REQUEST, keyPair.privateKey.serialize()) + val masterHexEncodedPublicKey = publicKeyEditText.text.trim().toString() + val authorisation = PairingAuthorisation(masterHexEncodedPublicKey, userHexEncodedPublicKey).sign(PairingAuthorisation.Type.REQUEST, keyPair.privateKey.serialize()) if (authorisation == null) { - Log.d("Loki", "Failed to sign outgoing pairing request.") + Log.d("Loki", "Failed to sign pairing request.") resetForRegistration() - return Toast.makeText(application, "Failed to link device.", Toast.LENGTH_SHORT).show() + return Toast.makeText(application, "Couldn't start device linking process.", Toast.LENGTH_SHORT).show() } val application = ApplicationContext.getInstance(this) application.startLongPollingIfNeeded() application.setUpP2PAPI() application.setUpStorageAPIIfNeeded() DeviceLinkingDialog.show(this, DeviceLinkingView.Mode.Slave, this) - CoroutineScope(Dispatchers.Main).launch { - retryIfNeeded(8) { - sendPairingAuthorisationMessage(this@SeedActivity, authorisation.primaryDevicePublicKey, authorisation).get() - } + retryIfNeeded(8) { + sendPairingAuthorisationMessage(this@SeedActivity, authorisation.primaryDevicePublicKey, authorisation).get() } } else { startActivity(Intent(this, DisplayNameActivity::class.java)) From 25bd1073b037e100b6f44d4d0320242a509f5313 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 10 Oct 2019 10:39:56 +1100 Subject: [PATCH 02/15] Pre-populate user ID cache as needed --- .../conversation/ConversationActivity.java | 3 ++ .../securesms/jobs/PushDecryptJob.java | 8 +++++ .../securesms/loki/LokiAPIUtilities.kt | 29 +++++++++++++++++++ .../securesms/loki/LokiUserDatabase.kt | 2 +- 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/org/thoughtcrime/securesms/loki/LokiAPIUtilities.kt diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index a3d1e1b30b..8cd329b12b 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -156,6 +156,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.loki.FriendRequestViewDelegate; +import org.thoughtcrime.securesms.loki.LokiAPIUtilities; import org.thoughtcrime.securesms.loki.LokiThreadDatabaseDelegate; import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mediasend.MediaSendActivity; @@ -394,6 +395,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } }); + LokiAPIUtilities.INSTANCE.populateUserIDCacheIfNeeded(threadId, this); + if (this.recipient.isGroupRecipient()) { if (this.recipient.getName().equals("Loki Public Chat")) { Analytics.Companion.getShared().track("Loki Public Chat Opened"); diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 9f141ec2aa..59a7ec1e27 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -67,6 +67,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.loki.LokiAPIUtilities; import org.thoughtcrime.securesms.loki.LokiMessageDatabase; import org.thoughtcrime.securesms.loki.LokiPreKeyBundleDatabase; import org.thoughtcrime.securesms.loki.LokiPreKeyRecordDatabase; @@ -123,6 +124,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.loki.api.DeviceLinkingSession; +import org.whispersystems.signalservice.loki.api.LokiAPI; import org.whispersystems.signalservice.loki.api.LokiStorageAPI; import org.whispersystems.signalservice.loki.api.PairingAuthorisation; import org.whispersystems.signalservice.loki.crypto.LokiServiceCipher; @@ -1021,6 +1023,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType { if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get()); + // Loki - Cache the user hex encoded public key (for mentions) + if (threadId != null) { + LokiAPIUtilities.INSTANCE.populateUserIDCacheIfNeeded(threadId, context); + LokiAPI.Companion.cache(textMessage.getSender().serialize(), threadId); + } + // Loki - Store message server ID updateGroupChatMessageServerID(messageServerIDOrNull, insertResult); diff --git a/src/org/thoughtcrime/securesms/loki/LokiAPIUtilities.kt b/src/org/thoughtcrime/securesms/loki/LokiAPIUtilities.kt new file mode 100644 index 0000000000..3c110606c5 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/LokiAPIUtilities.kt @@ -0,0 +1,29 @@ +package org.thoughtcrime.securesms.loki + +import android.content.Context +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.signalservice.loki.api.LokiAPI + +object LokiAPIUtilities { + + fun populateUserIDCacheIfNeeded(threadID: Long, context: Context) { + if (LokiAPI.userIDCache[threadID] != null) { return } + val result = mutableSetOf() + val messageDatabase = DatabaseFactory.getMmsSmsDatabase(context) + val reader = messageDatabase.readerFor(messageDatabase.getConversation(threadID)) + var record: MessageRecord? = reader.next + while (record != null) { + result.add(record.individualRecipient.address.serialize()) + try { + record = reader.next + } catch (exception: Exception) { + record = null + } + } + reader.close() + result.add(TextSecurePreferences.getLocalNumber(context)) + LokiAPI.userIDCache[threadID] = result + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/LokiUserDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiUserDatabase.kt index c84279b33e..a46307e48b 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiUserDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiUserDatabase.kt @@ -46,7 +46,7 @@ class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false).notifyListeners() } - fun getServerDisplayName(serverID: String, hexEncodedPublicKey: String): String? { + override fun getServerDisplayName(serverID: String, hexEncodedPublicKey: String): String? { val database = databaseHelper.readableDatabase return database.get(serverDisplayNameTable, "${Companion.hexEncodedPublicKey} = ? AND ${Companion.serverID} = ?", arrayOf( hexEncodedPublicKey, serverID )) { cursor -> cursor.getString(cursor.getColumnIndexOrThrow(displayName)) From 9207e479a6d2025f81e150cc2b7283bff5f1e222 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 10 Oct 2019 13:53:02 +1100 Subject: [PATCH 03/15] Implement user selection view --- res/layout/cell_user_selection_view.xml | 50 ++++++++++++++++++ res/layout/conversation_activity.xml | 21 +++++--- res/layout/conversation_input_panel.xml | 1 + res/layout/view_user_selection.xml | 7 +++ .../securesms/components/AvatarImageView.java | 5 ++ .../conversation/ConversationActivity.java | 7 +++ .../conversation/ConversationItem.java | 2 +- .../securesms/loki/UserSelectionView.kt | 52 +++++++++++++++++++ .../securesms/loki/UserSelectionViewCell.kt | 48 +++++++++++++++++ 9 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 res/layout/cell_user_selection_view.xml create mode 100644 res/layout/view_user_selection.xml create mode 100644 src/org/thoughtcrime/securesms/loki/UserSelectionView.kt create mode 100644 src/org/thoughtcrime/securesms/loki/UserSelectionViewCell.kt diff --git a/res/layout/cell_user_selection_view.xml b/res/layout/cell_user_selection_view.xml new file mode 100644 index 0000000000..366ddfa135 --- /dev/null +++ b/res/layout/cell_user_selection_view.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/conversation_activity.xml b/res/layout/conversation_activity.xml index fc87bafa80..f43acf74e9 100644 --- a/res/layout/conversation_activity.xml +++ b/res/layout/conversation_activity.xml @@ -71,17 +71,26 @@ android:inflatedId="@+id/attachment_editor" android:layout="@layout/conversation_activity_attachment_editor_stub" /> - + android:orientation="vertical"> - + - + - + + + + + + +