diff --git a/captures/network.loki.messenger_2019.10.11_14.38.li b/captures/network.loki.messenger_2019.10.11_14.38.li new file mode 100644 index 000000000..831192063 Binary files /dev/null and b/captures/network.loki.messenger_2019.10.11_14.38.li differ diff --git a/res/layout/cell_user_selection_view.xml b/res/layout/cell_mention_candidate_selection_view.xml similarity index 92% rename from res/layout/cell_user_selection_view.xml rename to res/layout/cell_mention_candidate_selection_view.xml index 0219e6d06..3ea8ade84 100644 --- a/res/layout/cell_user_selection_view.xml +++ b/res/layout/cell_mention_candidate_selection_view.xml @@ -1,5 +1,5 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/res/layout/conversation_activity.xml b/res/layout/conversation_activity.xml index f43acf74e..9c791f205 100644 --- a/res/layout/conversation_activity.xml +++ b/res/layout/conversation_activity.xml @@ -76,7 +76,7 @@ android:layout_height="wrap_content" android:orientation="vertical"> - + - mentions = new ArrayList<>(); private String oldText = ""; @@ -405,15 +404,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } composeText.setSelection(composeText.length(), composeText.length()); composeText.addTextChangedListener(mentionTextWatcher); - userSelectionView.setOnUserSelected(tuple -> { - Mention mention = new Mention(currentMentionStartIndex, tuple.getFirst(), tuple.getSecond()); - mentions.add(mention); + mentionCandidateSelectionView.setOnMentionCandidateSelected( mentionCandidate -> { + mentions.add(mentionCandidate); String oldText = composeText.getText().toString(); - String newText = oldText.substring(0, currentMentionStartIndex) + "@" + tuple.getSecond(); + String newText = oldText.substring(0, currentMentionStartIndex) + "@" + mentionCandidate.getDisplayName(); composeText.setText(newText); composeText.setSelection(newText.length()); - userSelectionView.hide(); currentMentionStartIndex = -1; + mentionCandidateSelectionView.hide(); + ConversationActivity.this.oldText = newText; return Unit.INSTANCE; }); } @@ -421,7 +420,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } }); - LokiAPIUtilities.INSTANCE.populateUserIDCacheIfNeeded(threadId, this); + LokiAPIUtilities.INSTANCE.populateUserHexEncodedPublicKeyCacheIfNeeded(threadId, this); if (this.recipient.isGroupRecipient()) { if (this.recipient.getName().equals("Loki Public Chat")) { @@ -1581,7 +1580,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity inlineAttachmentToggle = ViewUtil.findById(this, R.id.inline_attachment_container); inputPanel = ViewUtil.findById(this, R.id.bottom_panel); searchNav = ViewUtil.findById(this, R.id.conversation_search_nav); - userSelectionView = ViewUtil.findById(this, R.id.userSelectionView); + mentionCandidateSelectionView = ViewUtil.findById(this, R.id.userSelectionView); ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle); ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button); @@ -2093,15 +2092,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity private String getMessage() throws InvalidMessageException { String result = composeText.getTextTrimmed(); if (result.length() < 1 && !attachmentManager.isAttachmentPresent()) throw new InvalidMessageException(); - int shift = 0; for (Mention mention : mentions) { try { - int startIndex = mention.getLocationInString() + shift; + int startIndex = result.indexOf("@" + mention.getDisplayName()); int endIndex = startIndex + mention.getDisplayName().length() + 1; // + 1 to include the @ - shift = shift + mention.getHexEncodedPublicKey().length() - mention.getDisplayName().length(); result = result.substring(0, startIndex) + "@" + mention.getHexEncodedPublicKey() + result.substring(endIndex); } catch (Exception exception) { - // Do nothing + Log.d("Loki", "Couldn't process mention due to error: " + exception.toString() + "."); } } return result; @@ -2631,7 +2628,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity private void silentlySetComposeText(String text) { typingTextWatcher.setEnabled(false); composeText.setText(text); - if (text.isEmpty()) clearMentions(); + if (text.isEmpty()) resetMentions(); typingTextWatcher.setEnabled(true); } @@ -2767,43 +2764,43 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity boolean isBackspace = text.length() < oldText.length(); if (isBackspace) { currentMentionStartIndex = -1; - for (Mention mention : mentions) { - boolean isValid; - if (mention.getLocationInString() > (text.length() - 1)) { - isValid = false; - } else { - isValid = text.substring(mention.getLocationInString()).startsWith("@" + mention.getDisplayName()); - } - if (!isValid) { - mentions.remove(mention); + mentionCandidateSelectionView.hide(); + try { + for (Mention mention : mentions) { + if (!text.contains(mention.getDisplayName())) { + mentions.remove(mention); + } } + } catch (Exception exception) { + mentions.clear(); // TODO: Dirty workaround for ConcurrentModificationException } } else if (text.length() > 0) { if (currentMentionStartIndex > text.length()) { - clearMentions(); // Should never occur + resetMentions(); // Should never occur } - int currentEndIndex = text.length() - 1; - char lastCharacter = text.charAt(currentEndIndex); + int lastCharacterIndex = text.length() - 1; + char lastCharacter = text.charAt(lastCharacterIndex); LokiUserDatabase userDatabase = DatabaseFactory.getLokiUserDatabase(ConversationActivity.this); if (lastCharacter == '@') { - List> users = LokiAPI.Companion.getUsers("", threadId, userDatabase); - currentMentionStartIndex = currentEndIndex; - userSelectionView.show(users, threadId); + List mentionCandidates = LokiAPI.Companion.getMentionCandidates("", threadId, userDatabase); + currentMentionStartIndex = lastCharacterIndex; + mentionCandidateSelectionView.show(mentionCandidates, threadId); } else if (Character.isWhitespace(lastCharacter)) { currentMentionStartIndex = -1; - userSelectionView.hide(); + mentionCandidateSelectionView.hide(); } else { if (currentMentionStartIndex != -1) { String query = text.substring(currentMentionStartIndex + 1); // + 1 to get rid of the @ - List> users = LokiAPI.Companion.getUsers(query, threadId, userDatabase); - userSelectionView.show(users, threadId); + List mentionCandidates = LokiAPI.Companion.getMentionCandidates(query, threadId, userDatabase); + mentionCandidateSelectionView.show(mentionCandidates, threadId); } } } + ConversationActivity.this.oldText = text; } } - private void clearMentions() { + private void resetMentions() { oldText = ""; currentMentionStartIndex = -1; mentions.clear(); diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 59a7ec1e2..0007d8c87 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -1025,7 +1025,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { // Loki - Cache the user hex encoded public key (for mentions) if (threadId != null) { - LokiAPIUtilities.INSTANCE.populateUserIDCacheIfNeeded(threadId, context); + LokiAPIUtilities.INSTANCE.populateUserHexEncodedPublicKeyCacheIfNeeded(threadId, context); LokiAPI.Companion.cache(textMessage.getSender().serialize(), threadId); } diff --git a/src/org/thoughtcrime/securesms/loki/LokiAPIUtilities.kt b/src/org/thoughtcrime/securesms/loki/LokiAPIUtilities.kt index be5a8ecd1..aea9fd457 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiAPIUtilities.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiAPIUtilities.kt @@ -8,7 +8,7 @@ import org.whispersystems.signalservice.loki.api.LokiAPI object LokiAPIUtilities { - fun populateUserIDCacheIfNeeded(threadID: Long, context: Context) { + fun populateUserHexEncodedPublicKeyCacheIfNeeded(threadID: Long, context: Context) { if (LokiAPI.userHexEncodedPublicKeyCache[threadID] != null) { return } val result = mutableSetOf() val messageDatabase = DatabaseFactory.getMmsSmsDatabase(context) diff --git a/src/org/thoughtcrime/securesms/loki/Mention.kt b/src/org/thoughtcrime/securesms/loki/Mention.kt deleted file mode 100644 index e15cf44ef..000000000 --- a/src/org/thoughtcrime/securesms/loki/Mention.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.thoughtcrime.securesms.loki - -data class Mention(val locationInString: Int, val hexEncodedPublicKey: String, val displayName: String) \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/UserSelectionView.kt b/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionView.kt similarity index 56% rename from src/org/thoughtcrime/securesms/loki/UserSelectionView.kt rename to src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionView.kt index ec4a88dca..878c07cd9 100644 --- a/src/org/thoughtcrime/securesms/loki/UserSelectionView.kt +++ b/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionView.kt @@ -7,42 +7,42 @@ import android.view.View import android.view.ViewGroup import android.widget.BaseAdapter import android.widget.ListView -import nl.komponents.kovenant.combine.Tuple2 import org.thoughtcrime.securesms.database.DatabaseFactory +import org.whispersystems.signalservice.loki.messaging.Mention class UserSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) { - private var users = listOf>() - set(newValue) { field = newValue; userSelectionViewAdapter.users = newValue } + private var mentionCandidates = listOf() + set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.mentionCandidates = newValue } var publicChatServer: String? = null - set(newValue) { field = newValue; userSelectionViewAdapter.publicChatServer = publicChatServer } + set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.publicChatServer = publicChatServer } var publicChatChannel: Long? = null - set(newValue) { field = newValue; userSelectionViewAdapter.publicChatChannel = publicChatChannel } - var onUserSelected: ((Tuple2) -> Unit)? = null + set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.publicChatChannel = publicChatChannel } + var onMentionCandidateSelected: ((Mention) -> Unit)? = null - private val userSelectionViewAdapter by lazy { Adapter(context) } + private val mentionCandidateSelectionViewAdapter by lazy { Adapter(context) } private class Adapter(private val context: Context) : BaseAdapter() { - var users = listOf>() + var mentionCandidates = listOf() set(newValue) { field = newValue; notifyDataSetChanged() } var publicChatServer: String? = null var publicChatChannel: Long? = null override fun getCount(): Int { - return users.count() + return mentionCandidates.count() } override fun getItemId(position: Int): Long { return position.toLong() } - override fun getItem(position: Int): Tuple2 { - return users[position] + override fun getItem(position: Int): Mention { + return mentionCandidates[position] } override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View { - val cell = cellToBeReused as UserSelectionViewCell? ?: UserSelectionViewCell.inflate(LayoutInflater.from(context), parent) - val user = getItem(position) - cell.user = user + val cell = cellToBeReused as MentionCandidateSelectionViewCell? ?: MentionCandidateSelectionViewCell.inflate(LayoutInflater.from(context), parent) + val mentionCandidate = getItem(position) + cell.mentionCandidate = mentionCandidate cell.publicChatServer = publicChatServer cell.publicChatChannel = publicChatChannel return cell @@ -53,22 +53,22 @@ class UserSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: In constructor(context: Context) : this(context, null) init { - adapter = userSelectionViewAdapter - userSelectionViewAdapter.users = users + adapter = mentionCandidateSelectionViewAdapter + mentionCandidateSelectionViewAdapter.mentionCandidates = mentionCandidates setOnItemClickListener { _, _, position, _ -> - onUserSelected?.invoke(users[position]) + onMentionCandidateSelected?.invoke(mentionCandidates[position]) } } - fun show(users: List>, threadID: Long) { + fun show(mentionCandidates: List, threadID: Long) { val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) if (publicChat != null) { publicChatServer = publicChat.server publicChatChannel = publicChat.channel } - this.users = users + this.mentionCandidates = mentionCandidates val layoutParams = this.layoutParams as ViewGroup.LayoutParams - layoutParams.height = toPx(6 + Math.min(users.count(), 4) * 52, resources) + layoutParams.height = toPx(6 + Math.min(mentionCandidates.count(), 4) * 52, resources) this.layoutParams = layoutParams } diff --git a/src/org/thoughtcrime/securesms/loki/UserSelectionViewCell.kt b/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionViewCell.kt similarity index 70% rename from src/org/thoughtcrime/securesms/loki/UserSelectionViewCell.kt rename to src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionViewCell.kt index 30190d47f..36f571bb1 100644 --- a/src/org/thoughtcrime/securesms/loki/UserSelectionViewCell.kt +++ b/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionViewCell.kt @@ -8,13 +8,13 @@ import android.view.View import android.view.ViewGroup import android.view.ViewOutlineProvider import android.widget.LinearLayout -import kotlinx.android.synthetic.main.cell_user_selection_view.view.* +import kotlinx.android.synthetic.main.cell_mention_candidate_selection_view.view.* import network.loki.messenger.R -import nl.komponents.kovenant.combine.Tuple2 import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI +import org.whispersystems.signalservice.loki.messaging.Mention -class UserSelectionViewCell(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) { - var user = Tuple2("", "") +class MentionCandidateSelectionViewCell(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) { + var mentionCandidate = Mention("", "") set(newValue) { field = newValue; update() } var publicChatServer: String? = null var publicChatChannel: Long? = null @@ -24,8 +24,8 @@ class UserSelectionViewCell(context: Context, attrs: AttributeSet?, defStyleAttr companion object { - fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): UserSelectionViewCell { - return layoutInflater.inflate(R.layout.cell_user_selection_view, parent, false) as UserSelectionViewCell + fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): MentionCandidateSelectionViewCell { + return layoutInflater.inflate(R.layout.cell_mention_candidate_selection_view, parent, false) as MentionCandidateSelectionViewCell } } @@ -41,8 +41,8 @@ class UserSelectionViewCell(context: Context, attrs: AttributeSet?, defStyleAttr } private fun update() { - displayNameTextView.text = user.second - profilePictureImageView.update(user.first) + displayNameTextView.text = mentionCandidate.displayName + profilePictureImageView.update(mentionCandidate.hexEncodedPublicKey) if (publicChatServer != null && publicChatChannel != null) { val isUserModerator = LokiPublicChatAPI.isUserModerator(user.first, publicChatChannel!!, publicChatServer!!) moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE diff --git a/src/org/thoughtcrime/securesms/loki/MentionUtilities.kt b/src/org/thoughtcrime/securesms/loki/MentionUtilities.kt index 38e847553..82ded332c 100644 --- a/src/org/thoughtcrime/securesms/loki/MentionUtilities.kt +++ b/src/org/thoughtcrime/securesms/loki/MentionUtilities.kt @@ -27,13 +27,13 @@ object MentionUtilities { val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) if (matcher.find(startIndex)) { while (true) { - val userID = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @ - val userDisplayName: String? = if (userID.toLowerCase() == TextSecurePreferences.getLocalNumber(context).toLowerCase()) { + val hexEncodedPublicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @ + val userDisplayName: String? = if (hexEncodedPublicKey.toLowerCase() == TextSecurePreferences.getLocalNumber(context).toLowerCase()) { TextSecurePreferences.getProfileName(context) } else if (publicChat != null) { - DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.id, userID) + DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.id, hexEncodedPublicKey) } else { - "" // TODO: Implement + DatabaseFactory.getLokiUserDatabase(context).getDisplayName(hexEncodedPublicKey) } if (userDisplayName != null) { text = text.subSequence(0, matcher.start()).toString() + "@" + userDisplayName + text.subSequence(matcher.end(), text.length)