Change mentions approach

This commit is contained in:
Niels Andriesse 2019-10-11 16:37:28 +11:00
parent ca9032e109
commit e423ec8848
10 changed files with 71 additions and 77 deletions

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.loki.UserSelectionViewCell <org.thoughtcrime.securesms.loki.MentionCandidateSelectionViewCell
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="52dp" android:layout_height="52dp"
@ -48,4 +48,4 @@
style="@style/Signal.Text.Body" style="@style/Signal.Text.Body"
android:ellipsize="end" /> android:ellipsize="end" />
</org.thoughtcrime.securesms.loki.UserSelectionViewCell> </org.thoughtcrime.securesms.loki.MentionCandidateSelectionViewCell>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.loki.UserSelectionView <org.thoughtcrime.securesms.loki.MentionCandidateSelectionView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/userSelectionView" android:id="@+id/userSelectionView"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -272,7 +272,7 @@ public class ConversationListItem extends RelativeLayout
} }
private @NonNull CharSequence getTrimmedSnippet(@NonNull CharSequence snippet) { private @NonNull CharSequence getTrimmedSnippet(@NonNull CharSequence snippet) {
LokiAPIUtilities.INSTANCE.populateUserIDCacheIfNeeded(threadId, getContext()); // TODO: Terrible place to do this, but okay for now LokiAPIUtilities.INSTANCE.populateUserHexEncodedPublicKeyCacheIfNeeded(threadId, getContext()); // TODO: Terrible place to do this, but okay for now
snippet = MentionUtilities.highlightMentions(snippet, this.recipient.isGroupRecipient(), getContext()); snippet = MentionUtilities.highlightMentions(snippet, this.recipient.isGroupRecipient(), getContext());
return snippet.length() <= MAX_SNIPPET_LENGTH ? snippet : snippet.subSequence(0, MAX_SNIPPET_LENGTH); return snippet.length() <= MAX_SNIPPET_LENGTH ? snippet : snippet.subSequence(0, MAX_SNIPPET_LENGTH);
} }

View File

@ -159,8 +159,7 @@ import org.thoughtcrime.securesms.loki.FriendRequestViewDelegate;
import org.thoughtcrime.securesms.loki.LokiAPIUtilities; import org.thoughtcrime.securesms.loki.LokiAPIUtilities;
import org.thoughtcrime.securesms.loki.LokiThreadDatabaseDelegate; import org.thoughtcrime.securesms.loki.LokiThreadDatabaseDelegate;
import org.thoughtcrime.securesms.loki.LokiUserDatabase; import org.thoughtcrime.securesms.loki.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.Mention; import org.thoughtcrime.securesms.loki.MentionCandidateSelectionView;
import org.thoughtcrime.securesms.loki.UserSelectionView;
import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaSendActivity; import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
import org.thoughtcrime.securesms.mms.AttachmentManager; import org.thoughtcrime.securesms.mms.AttachmentManager;
@ -231,6 +230,7 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.loki.api.LokiAPI; import org.whispersystems.signalservice.loki.api.LokiAPI;
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus; import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus;
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus; import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
import org.whispersystems.signalservice.loki.messaging.Mention;
import org.whispersystems.signalservice.loki.utilities.Analytics; import org.whispersystems.signalservice.loki.utilities.Analytics;
import java.io.IOException; import java.io.IOException;
@ -247,7 +247,6 @@ import java.util.concurrent.atomic.AtomicInteger;
import kotlin.Unit; import kotlin.Unit;
import network.loki.messenger.R; import network.loki.messenger.R;
import nl.komponents.kovenant.combine.Tuple2;
import static org.thoughtcrime.securesms.TransportOption.Type; import static org.thoughtcrime.securesms.TransportOption.Type;
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
@ -348,7 +347,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
// Mentions // Mentions
private UserSelectionView userSelectionView; private MentionCandidateSelectionView mentionCandidateSelectionView;
private int currentMentionStartIndex = -1; private int currentMentionStartIndex = -1;
private ArrayList<Mention> mentions = new ArrayList<>(); private ArrayList<Mention> mentions = new ArrayList<>();
private String oldText = ""; private String oldText = "";
@ -405,15 +404,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
composeText.setSelection(composeText.length(), composeText.length()); composeText.setSelection(composeText.length(), composeText.length());
composeText.addTextChangedListener(mentionTextWatcher); composeText.addTextChangedListener(mentionTextWatcher);
userSelectionView.setOnUserSelected(tuple -> { mentionCandidateSelectionView.setOnMentionCandidateSelected( mentionCandidate -> {
Mention mention = new Mention(currentMentionStartIndex, tuple.getFirst(), tuple.getSecond()); mentions.add(mentionCandidate);
mentions.add(mention);
String oldText = composeText.getText().toString(); 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.setText(newText);
composeText.setSelection(newText.length()); composeText.setSelection(newText.length());
userSelectionView.hide();
currentMentionStartIndex = -1; currentMentionStartIndex = -1;
mentionCandidateSelectionView.hide();
ConversationActivity.this.oldText = newText;
return Unit.INSTANCE; 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.isGroupRecipient()) {
if (this.recipient.getName().equals("Loki Public Chat")) { 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); inlineAttachmentToggle = ViewUtil.findById(this, R.id.inline_attachment_container);
inputPanel = ViewUtil.findById(this, R.id.bottom_panel); inputPanel = ViewUtil.findById(this, R.id.bottom_panel);
searchNav = ViewUtil.findById(this, R.id.conversation_search_nav); 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 quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle);
ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button); ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button);
@ -2093,15 +2092,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private String getMessage() throws InvalidMessageException { private String getMessage() throws InvalidMessageException {
String result = composeText.getTextTrimmed(); String result = composeText.getTextTrimmed();
if (result.length() < 1 && !attachmentManager.isAttachmentPresent()) throw new InvalidMessageException(); if (result.length() < 1 && !attachmentManager.isAttachmentPresent()) throw new InvalidMessageException();
int shift = 0;
for (Mention mention : mentions) { for (Mention mention : mentions) {
try { try {
int startIndex = mention.getLocationInString() + shift; int startIndex = result.indexOf("@" + mention.getDisplayName());
int endIndex = startIndex + mention.getDisplayName().length() + 1; // + 1 to include the @ 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); result = result.substring(0, startIndex) + "@" + mention.getHexEncodedPublicKey() + result.substring(endIndex);
} catch (Exception exception) { } catch (Exception exception) {
// Do nothing Log.d("Loki", "Couldn't process mention due to error: " + exception.toString() + ".");
} }
} }
return result; return result;
@ -2631,7 +2628,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void silentlySetComposeText(String text) { private void silentlySetComposeText(String text) {
typingTextWatcher.setEnabled(false); typingTextWatcher.setEnabled(false);
composeText.setText(text); composeText.setText(text);
if (text.isEmpty()) clearMentions(); if (text.isEmpty()) resetMentions();
typingTextWatcher.setEnabled(true); typingTextWatcher.setEnabled(true);
} }
@ -2767,43 +2764,43 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
boolean isBackspace = text.length() < oldText.length(); boolean isBackspace = text.length() < oldText.length();
if (isBackspace) { if (isBackspace) {
currentMentionStartIndex = -1; currentMentionStartIndex = -1;
mentionCandidateSelectionView.hide();
try {
for (Mention mention : mentions) { for (Mention mention : mentions) {
boolean isValid; if (!text.contains(mention.getDisplayName())) {
if (mention.getLocationInString() > (text.length() - 1)) {
isValid = false;
} else {
isValid = text.substring(mention.getLocationInString()).startsWith("@" + mention.getDisplayName());
}
if (!isValid) {
mentions.remove(mention); mentions.remove(mention);
} }
} }
} catch (Exception exception) {
mentions.clear(); // TODO: Dirty workaround for ConcurrentModificationException
}
} else if (text.length() > 0) { } else if (text.length() > 0) {
if (currentMentionStartIndex > text.length()) { if (currentMentionStartIndex > text.length()) {
clearMentions(); // Should never occur resetMentions(); // Should never occur
} }
int currentEndIndex = text.length() - 1; int lastCharacterIndex = text.length() - 1;
char lastCharacter = text.charAt(currentEndIndex); char lastCharacter = text.charAt(lastCharacterIndex);
LokiUserDatabase userDatabase = DatabaseFactory.getLokiUserDatabase(ConversationActivity.this); LokiUserDatabase userDatabase = DatabaseFactory.getLokiUserDatabase(ConversationActivity.this);
if (lastCharacter == '@') { if (lastCharacter == '@') {
List<Tuple2<String, String>> users = LokiAPI.Companion.getUsers("", threadId, userDatabase); List<Mention> mentionCandidates = LokiAPI.Companion.getMentionCandidates("", threadId, userDatabase);
currentMentionStartIndex = currentEndIndex; currentMentionStartIndex = lastCharacterIndex;
userSelectionView.show(users, threadId); mentionCandidateSelectionView.show(mentionCandidates, threadId);
} else if (Character.isWhitespace(lastCharacter)) { } else if (Character.isWhitespace(lastCharacter)) {
currentMentionStartIndex = -1; currentMentionStartIndex = -1;
userSelectionView.hide(); mentionCandidateSelectionView.hide();
} else { } else {
if (currentMentionStartIndex != -1) { if (currentMentionStartIndex != -1) {
String query = text.substring(currentMentionStartIndex + 1); // + 1 to get rid of the @ String query = text.substring(currentMentionStartIndex + 1); // + 1 to get rid of the @
List<Tuple2<String, String>> users = LokiAPI.Companion.getUsers(query, threadId, userDatabase); List<Mention> mentionCandidates = LokiAPI.Companion.getMentionCandidates(query, threadId, userDatabase);
userSelectionView.show(users, threadId); mentionCandidateSelectionView.show(mentionCandidates, threadId);
} }
} }
} }
ConversationActivity.this.oldText = text;
} }
} }
private void clearMentions() { private void resetMentions() {
oldText = ""; oldText = "";
currentMentionStartIndex = -1; currentMentionStartIndex = -1;
mentions.clear(); mentions.clear();

View File

@ -1025,7 +1025,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
// Loki - Cache the user hex encoded public key (for mentions) // Loki - Cache the user hex encoded public key (for mentions)
if (threadId != null) { if (threadId != null) {
LokiAPIUtilities.INSTANCE.populateUserIDCacheIfNeeded(threadId, context); LokiAPIUtilities.INSTANCE.populateUserHexEncodedPublicKeyCacheIfNeeded(threadId, context);
LokiAPI.Companion.cache(textMessage.getSender().serialize(), threadId); LokiAPI.Companion.cache(textMessage.getSender().serialize(), threadId);
} }

View File

@ -8,8 +8,8 @@ import org.whispersystems.signalservice.loki.api.LokiAPI
object LokiAPIUtilities { object LokiAPIUtilities {
fun populateUserIDCacheIfNeeded(threadID: Long, context: Context) { fun populateUserHexEncodedPublicKeyCacheIfNeeded(threadID: Long, context: Context) {
if (LokiAPI.userIDCache[threadID] != null) { return } if (LokiAPI.userHexEncodedPublicKeyCache[threadID] != null) { return }
val result = mutableSetOf<String>() val result = mutableSetOf<String>()
val messageDatabase = DatabaseFactory.getMmsSmsDatabase(context) val messageDatabase = DatabaseFactory.getMmsSmsDatabase(context)
val reader = messageDatabase.readerFor(messageDatabase.getConversation(threadID)) val reader = messageDatabase.readerFor(messageDatabase.getConversation(threadID))
@ -24,6 +24,6 @@ object LokiAPIUtilities {
} }
reader.close() reader.close()
result.add(TextSecurePreferences.getLocalNumber(context)) result.add(TextSecurePreferences.getLocalNumber(context))
LokiAPI.userIDCache[threadID] = result LokiAPI.userHexEncodedPublicKeyCache[threadID] = result
} }
} }

View File

@ -1,3 +0,0 @@
package org.thoughtcrime.securesms.loki
data class Mention(val locationInString: Int, val hexEncodedPublicKey: String, val displayName: String)

View File

@ -7,38 +7,38 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.BaseAdapter import android.widget.BaseAdapter
import android.widget.ListView import android.widget.ListView
import nl.komponents.kovenant.combine.Tuple2
import org.thoughtcrime.securesms.database.DatabaseFactory 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) { class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) {
private var users = listOf<Tuple2<String, String>>() private var mentionCandidates = listOf<Mention>()
set(newValue) { field = newValue; userSelectionViewAdapter.users = newValue } set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.mentionCandidates = newValue }
private var hasGroupContext = false private var hasGroupContext = false
var onUserSelected: ((Tuple2<String, String>) -> Unit)? = null 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() { private class Adapter(private val context: Context) : BaseAdapter() {
var users = listOf<Tuple2<String, String>>() var mentionCandidates = listOf<Mention>()
set(newValue) { field = newValue; notifyDataSetChanged() } set(newValue) { field = newValue; notifyDataSetChanged() }
var hasGroupContext = false var hasGroupContext = false
override fun getCount(): Int { override fun getCount(): Int {
return users.count() return mentionCandidates.count()
} }
override fun getItemId(position: Int): Long { override fun getItemId(position: Int): Long {
return position.toLong() return position.toLong()
} }
override fun getItem(position: Int): Tuple2<String, String> { override fun getItem(position: Int): Mention {
return users[position] return mentionCandidates[position]
} }
override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View { override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View {
val cell = cellToBeReused as UserSelectionViewCell? ?: UserSelectionViewCell.inflate(LayoutInflater.from(context), parent) val cell = cellToBeReused as MentionCandidateSelectionViewCell? ?: MentionCandidateSelectionViewCell.inflate(LayoutInflater.from(context), parent)
val user = getItem(position) val mentionCandidate = getItem(position)
cell.user = user cell.mentionCandidate = mentionCandidate
cell.hasGroupContext = hasGroupContext cell.hasGroupContext = hasGroupContext
return cell return cell
} }
@ -48,18 +48,18 @@ class UserSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: In
constructor(context: Context) : this(context, null) constructor(context: Context) : this(context, null)
init { init {
adapter = userSelectionViewAdapter adapter = mentionCandidateSelectionViewAdapter
userSelectionViewAdapter.users = users mentionCandidateSelectionViewAdapter.mentionCandidates = mentionCandidates
setOnItemClickListener { _, _, position, _ -> setOnItemClickListener { _, _, position, _ ->
onUserSelected?.invoke(users[position]) onMentionCandidateSelected?.invoke(mentionCandidates[position])
} }
} }
fun show(users: List<Tuple2<String, String>>, threadID: Long) { fun show(mentionCandidates: List<Mention>, threadID: Long) {
hasGroupContext = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadID)!!.isGroupRecipient hasGroupContext = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadID)!!.isGroupRecipient
this.users = users this.mentionCandidates = mentionCandidates
val layoutParams = this.layoutParams as ViewGroup.LayoutParams 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 this.layoutParams = layoutParams
} }

View File

@ -8,13 +8,13 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewOutlineProvider import android.view.ViewOutlineProvider
import android.widget.LinearLayout 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 network.loki.messenger.R
import nl.komponents.kovenant.combine.Tuple2
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI
import org.whispersystems.signalservice.loki.messaging.Mention
class UserSelectionViewCell(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) { class MentionCandidateSelectionViewCell(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {
var user = Tuple2("", "") var mentionCandidate = Mention("", "")
set(newValue) { field = newValue; update() } set(newValue) { field = newValue; update() }
var hasGroupContext = false var hasGroupContext = false
@ -23,8 +23,8 @@ class UserSelectionViewCell(context: Context, attrs: AttributeSet?, defStyleAttr
companion object { companion object {
fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): UserSelectionViewCell { fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): MentionCandidateSelectionViewCell {
return layoutInflater.inflate(R.layout.cell_user_selection_view, parent, false) as UserSelectionViewCell return layoutInflater.inflate(R.layout.cell_mention_candidate_selection_view, parent, false) as MentionCandidateSelectionViewCell
} }
} }
@ -40,9 +40,9 @@ class UserSelectionViewCell(context: Context, attrs: AttributeSet?, defStyleAttr
} }
private fun update() { private fun update() {
displayNameTextView.text = user.second displayNameTextView.text = mentionCandidate.displayName
profilePictureImageView.update(user.first) profilePictureImageView.update(mentionCandidate.hexEncodedPublicKey)
val isUserModerator = LokiGroupChatAPI.isUserModerator(user.first, LokiGroupChatAPI.publicChatServerID, LokiGroupChatAPI.publicChatServer) val isUserModerator = LokiGroupChatAPI.isUserModerator(mentionCandidate.hexEncodedPublicKey, LokiGroupChatAPI.publicChatServerID, LokiGroupChatAPI.publicChatServer)
moderatorIconImageView.visibility = if (isUserModerator && hasGroupContext) View.VISIBLE else View.GONE moderatorIconImageView.visibility = if (isUserModerator && hasGroupContext) View.VISIBLE else View.GONE
} }
} }

View File

@ -27,12 +27,12 @@ object MentionUtilities {
var startIndex = 0 var startIndex = 0
if (matcher.find(startIndex) && isGroupThread) { if (matcher.find(startIndex) && isGroupThread) {
while (true) { while (true) {
val userID = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @ val hexEncodedPublicKey = 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 userDisplayName: String? = if (hexEncodedPublicKey.toLowerCase() == TextSecurePreferences.getLocalNumber(context).toLowerCase()) {
TextSecurePreferences.getProfileName(context) TextSecurePreferences.getProfileName(context)
} else { } else {
val publicChatID = LokiGroupChatAPI.publicChatServer + "." + LokiGroupChatAPI.publicChatServerID val publicChatID = LokiGroupChatAPI.publicChatServer + "." + LokiGroupChatAPI.publicChatServerID
DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChatID, userID) DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChatID, hexEncodedPublicKey)
} }
if (userDisplayName != null) { if (userDisplayName != null) {
text = text.subSequence(0, matcher.start()).toString() + "@" + userDisplayName + text.subSequence(matcher.end(), text.length) text = text.subSequence(0, matcher.start()).toString() + "@" + userDisplayName + text.subSequence(matcher.end(), text.length)