Merge branch 'dev' into custom-server

# Conflicts:
#	src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt
This commit is contained in:
Mikunj 2019-10-11 15:40:09 +11:00
commit 2f18c5bad2
25 changed files with 469 additions and 91 deletions

View File

@ -2,10 +2,8 @@
<ScrollView <ScrollView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
tools:context=".loki.AccountDetailsActivity">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.loki.UserSelectionViewCell
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="52dp"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:gravity="center_vertical"
android:background="?attr/conversation_list_item_background">
<RelativeLayout
android:layout_width="36dp"
android:layout_height="38dp"
android:layout_marginTop="1dp">
<!-- The frame layout below is a workaround for an avatar image view bug -->
<FrameLayout
android:id="@+id/profilePictureImageViewContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/profilePictureImageView"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginBottom="2dp" />
</FrameLayout>
<ImageView
android:id="@+id/moderatorIconImageView"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="1.5dp"
android:src="@drawable/icon_crown"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true" />
</RelativeLayout>
<TextView
android:id="@+id/displayNameTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
style="@style/Signal.Text.Body"
android:ellipsize="end" />
</org.thoughtcrime.securesms.loki.UserSelectionViewCell>

View File

@ -71,17 +71,26 @@
android:inflatedId="@+id/attachment_editor" android:inflatedId="@+id/attachment_editor"
android:layout="@layout/conversation_activity_attachment_editor_stub" /> android:layout="@layout/conversation_activity_attachment_editor_stub" />
<FrameLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipChildren="false" android:orientation="vertical">
android:clipToPadding="false">
<include layout="@layout/conversation_input_panel" /> <include layout="@layout/view_user_selection" />
<include layout="@layout/conversation_search_nav" /> <FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false">
</FrameLayout> <include layout="@layout/conversation_input_panel" />
<include layout="@layout/conversation_search_nav" />
</FrameLayout>
</LinearLayout>
<Button <Button
android:id="@+id/register_button" android:id="@+id/register_button"

View File

@ -101,6 +101,7 @@
tools:visibility="invisible" tools:visibility="invisible"
tools:hint="Send TextSecure message" > tools:hint="Send TextSecure message" >
<requestFocus /> <requestFocus />
</org.thoughtcrime.securesms.components.ComposeText> </org.thoughtcrime.securesms.components.ComposeText>
<FrameLayout <FrameLayout

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.loki.UserSelectionView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/userSelectionView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:paddingTop="6dp"
android:listSelector="@color/transparent" />

View File

@ -2,6 +2,7 @@
<resources> <resources>
<!-- Loki --> <!-- Loki -->
<color name="loki_green">#78be20</color> <color name="loki_green">#78be20</color>
<color name="loki_dark_green">#419B41</color>
<color name="loki_darkest_gray">#0a0a0a</color> <color name="loki_darkest_gray">#0a0a0a</color>
<!-- Loki --> <!-- Loki -->

View File

@ -1552,7 +1552,7 @@
<!-- Display name activity --> <!-- Display name activity -->
<string name="activity_display_name_title">Create Your Loki Messenger Account</string> <string name="activity_display_name_title">Create Your Loki Messenger Account</string>
<string name="activity_display_name_subtitle">Enter a name to be shown to your contacts</string> <string name="activity_display_name_subtitle">Enter a name to be shown to your contacts</string>
<string name="activity_display_name_name_edit_text_label">Display Name (Optional)</string> <string name="activity_display_name_name_edit_text_label">Display Name</string>
<string name="activity_display_name_button_title">Next</string> <string name="activity_display_name_button_title">Next</string>
<!-- Key pair activity --> <!-- Key pair activity -->
<string name="activity_key_pair_title">Create Your Loki Messenger Account</string> <string name="activity_key_pair_title">Create Your Loki Messenger Account</string>

View File

@ -160,7 +160,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
super.onCreate(icicle); super.onCreate(icicle);
String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(getContext()); String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(getContext());
boolean isMasterDevice = (masterHexEncodedPublicKey != null); boolean isMasterDevice = (masterHexEncodedPublicKey == null);
Preference profilePreference = this.findPreference(PREFERENCE_CATEGORY_PROFILE); Preference profilePreference = this.findPreference(PREFERENCE_CATEGORY_PROFILE);
// Hide if this is a slave device // Hide if this is a slave device

View File

@ -203,7 +203,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
int height = profilePictureImageView.getHeight(); int height = profilePictureImageView.getHeight();
if (width == 0 || height == 0) return true; if (width == 0 || height == 0) return true;
profilePictureImageView.getViewTreeObserver().removeOnPreDrawListener(this); profilePictureImageView.getViewTreeObserver().removeOnPreDrawListener(this);
JazzIdenticonDrawable identicon = new JazzIdenticonDrawable(width, height, recipient.getAddress().serialize()); JazzIdenticonDrawable identicon = new JazzIdenticonDrawable(width, height, recipient.getAddress().serialize().toLowerCase());
profilePictureImageView.setImageDrawable(identicon); profilePictureImageView.setImageDrawable(identicon);
return true; return true;
} }

View File

@ -37,6 +37,8 @@ import org.thoughtcrime.securesms.components.FromTextView;
import org.thoughtcrime.securesms.components.ThumbnailView; import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.components.TypingIndicatorView; import org.thoughtcrime.securesms.components.TypingIndicatorView;
import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.loki.LokiAPIUtilities;
import org.thoughtcrime.securesms.loki.MentionUtilities;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
@ -270,8 +272,9 @@ public class ConversationListItem extends RelativeLayout
} }
private @NonNull CharSequence getTrimmedSnippet(@NonNull CharSequence snippet) { private @NonNull CharSequence getTrimmedSnippet(@NonNull CharSequence snippet) {
return snippet.length() <= MAX_SNIPPET_LENGTH ? snippet LokiAPIUtilities.INSTANCE.populateUserIDCacheIfNeeded(threadId, getContext()); // TODO: Terrible place to do this, but okay for now
: snippet.subSequence(0, MAX_SNIPPET_LENGTH); snippet = MentionUtilities.highlightMentions(snippet, this.recipient.isGroupRecipient(), getContext());
return snippet.length() <= MAX_SNIPPET_LENGTH ? snippet : snippet.subSequence(0, MAX_SNIPPET_LENGTH);
} }
private void setThumbnailSnippet(ThreadRecord thread) { private void setThumbnailSnippet(ThreadRecord thread) {

View File

@ -232,7 +232,10 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
public void onTextChanged(CharSequence s, int start, int before, int count) {} public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override @Override
public void afterTextChanged(Editable s) { public void afterTextChanged(Editable s) {
if (s.toString().getBytes().length > ProfileCipher.NAME_PADDED_LENGTH) { if (s.toString().isEmpty()) {
name.getInput().setError("Invalid");
finishButton.setEnabled(false);
} else if (s.toString().getBytes().length > ProfileCipher.NAME_PADDED_LENGTH) {
name.getInput().setError(getString(R.string.CreateProfileActivity_too_long)); name.getInput().setError(getString(R.string.CreateProfileActivity_too_long));
finishButton.setEnabled(false); finishButton.setEnabled(false);
} else if (name.getInput().getError() != null || !finishButton.isEnabled()) { } else if (name.getInput().getError() != null || !finishButton.isEnabled()) {

View File

@ -19,6 +19,7 @@ import android.view.ViewOutlineProvider;
import org.thoughtcrime.securesms.color.MaterialColor; import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors; import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.loki.JazzIdenticonDrawable; import org.thoughtcrime.securesms.loki.JazzIdenticonDrawable;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@ -118,11 +119,15 @@ public class AvatarImageView extends AppCompatImageView {
image = new GeneratedContactPhoto(name, R.drawable.ic_profile_default).asDrawable(context, fallbackColor.toAvatarColor(context)); image = new GeneratedContactPhoto(name, R.drawable.ic_profile_default).asDrawable(context, fallbackColor.toAvatarColor(context));
} else { } else {
image = new JazzIdenticonDrawable(w, h, recipient.getAddress().serialize()); image = new JazzIdenticonDrawable(w, h, recipient.getAddress().serialize().toLowerCase());
} }
setImageDrawable(image); setImageDrawable(image);
} }
public void update(String hexEncodedPublicKey) {
this.recipient = Recipient.from(getContext(), Address.fromSerialized(hexEncodedPublicKey), false);
}
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) { public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) {
this.recipient = recipient; this.recipient = recipient;
/* /*

View File

@ -156,7 +156,11 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel; import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.FriendRequestViewDelegate; import org.thoughtcrime.securesms.loki.FriendRequestViewDelegate;
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.Mention;
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;
@ -224,12 +228,14 @@ import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
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.utilities.Analytics; import org.whispersystems.signalservice.loki.utilities.Analytics;
import java.io.IOException; import java.io.IOException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.LinkedList; import java.util.LinkedList;
@ -239,7 +245,9 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
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;
@ -292,23 +300,24 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private static final int SMS_DEFAULT = 11; private static final int SMS_DEFAULT = 11;
private static final int MEDIA_SENDER = 12; private static final int MEDIA_SENDER = 12;
private GlideRequests glideRequests; private GlideRequests glideRequests;
protected ComposeText composeText; protected ComposeText composeText;
private AnimatingToggle buttonToggle; private AnimatingToggle buttonToggle;
private SendButton sendButton; private SendButton sendButton;
private ImageButton attachButton; private ImageButton attachButton;
protected ConversationTitleView titleView; protected ConversationTitleView titleView;
private TextView charactersLeft; private TextView charactersLeft;
private ConversationFragment fragment; private ConversationFragment fragment;
private Button unblockButton; private Button unblockButton;
private Button makeDefaultSmsButton; private Button makeDefaultSmsButton;
private Button registerButton; private Button registerButton;
private InputAwareLayout container; private InputAwareLayout container;
private View composePanel; private View composePanel;
protected Stub<ReminderView> reminderView; protected Stub<ReminderView> reminderView;
private Stub<UnverifiedBannerView> unverifiedBannerView; private Stub<UnverifiedBannerView> unverifiedBannerView;
private Stub<GroupShareProfileView> groupShareProfileView; private Stub<GroupShareProfileView> groupShareProfileView;
private TypingStatusTextWatcher typingTextWatcher; private TypingStatusTextWatcher typingTextWatcher;
private MentionTextWatcher mentionTextWatcher;
private ConversationSearchBottomBar searchNav; private ConversationSearchBottomBar searchNav;
private MenuItem searchViewItem; private MenuItem searchViewItem;
@ -338,6 +347,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private final DynamicNoActionBarTheme dynamicTheme = new DynamicNoActionBarTheme(); private final DynamicNoActionBarTheme dynamicTheme = new DynamicNoActionBarTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
// Mentions
private UserSelectionView userSelectionView;
private int currentMentionStartIndex = -1;
private ArrayList<Mention> mentions = new ArrayList<>();
private String oldText = "";
@Override @Override
protected void onPreCreate() { protected void onPreCreate() {
@ -389,11 +404,25 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
composeText.addTextChangedListener(typingTextWatcher); composeText.addTextChangedListener(typingTextWatcher);
} }
composeText.setSelection(composeText.length(), composeText.length()); composeText.setSelection(composeText.length(), composeText.length());
composeText.addTextChangedListener(mentionTextWatcher);
userSelectionView.setOnUserSelected(tuple -> {
Mention mention = new Mention(currentMentionStartIndex, tuple.getFirst(), tuple.getSecond());
mentions.add(mention);
String oldText = composeText.getText().toString();
String newText = oldText.substring(0, currentMentionStartIndex) + "@" + tuple.getSecond();
composeText.setText(newText);
composeText.setSelection(newText.length());
userSelectionView.hide();
currentMentionStartIndex = -1;
return Unit.INSTANCE;
});
} }
}); });
} }
}); });
LokiAPIUtilities.INSTANCE.populateUserIDCacheIfNeeded(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")) {
Analytics.Companion.getShared().track("Loki Public Chat Opened"); Analytics.Companion.getShared().track("Loki Public Chat Opened");
@ -1552,6 +1581,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);
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);
@ -1564,6 +1594,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
attachmentManager = new AttachmentManager(this, this); attachmentManager = new AttachmentManager(this, this);
audioRecorder = new AudioRecorder(this); audioRecorder = new AudioRecorder(this);
typingTextWatcher = new TypingStatusTextWatcher(); typingTextWatcher = new TypingStatusTextWatcher();
mentionTextWatcher = new MentionTextWatcher();
SendButtonListener sendButtonListener = new SendButtonListener(); SendButtonListener sendButtonListener = new SendButtonListener();
ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener(); ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener();
@ -2060,12 +2091,20 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
private String getMessage() throws InvalidMessageException { private String getMessage() throws InvalidMessageException {
String rawText = composeText.getTextTrimmed(); String result = composeText.getTextTrimmed();
if (result.length() < 1 && !attachmentManager.isAttachmentPresent()) throw new InvalidMessageException();
if (rawText.length() < 1 && !attachmentManager.isAttachmentPresent()) int shift = 0;
throw new InvalidMessageException(getString(R.string.ConversationActivity_message_is_empty_exclamation)); for (Mention mention : mentions) {
try {
return rawText; int startIndex = mention.getLocationInString() + shift;
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
}
}
return result;
} }
private Pair<String, Optional<Slide>> getSplitMessage(String rawText, int maxPrimaryMessageSize) { private Pair<String, Optional<Slide>> getSplitMessage(String rawText, int maxPrimaryMessageSize) {
@ -2592,6 +2631,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();
typingTextWatcher.setEnabled(true); typingTextWatcher.setEnabled(true);
} }
@ -2706,7 +2746,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
private class TypingStatusTextWatcher extends SimpleTextWatcher { private class TypingStatusTextWatcher extends SimpleTextWatcher {
private boolean enabled = true; private boolean enabled = true;
@Override @Override
@ -2721,6 +2760,55 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
} }
private class MentionTextWatcher extends SimpleTextWatcher {
@Override
public void onTextChanged(String text) {
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);
}
}
} else if (text.length() > 0) {
if (currentMentionStartIndex > text.length()) {
clearMentions(); // Should never occur
}
int currentEndIndex = text.length() - 1;
char lastCharacter = text.charAt(currentEndIndex);
LokiUserDatabase userDatabase = DatabaseFactory.getLokiUserDatabase(ConversationActivity.this);
if (lastCharacter == '@') {
List<Tuple2<String, String>> users = LokiAPI.Companion.getUsers("", threadId, userDatabase);
currentMentionStartIndex = currentEndIndex;
userSelectionView.show(users, threadId);
} else if (Character.isWhitespace(lastCharacter)) {
currentMentionStartIndex = -1;
userSelectionView.hide();
} else {
if (currentMentionStartIndex != -1) {
String query = text.substring(currentMentionStartIndex + 1); // + 1 to get rid of the @
List<Tuple2<String, String>> users = LokiAPI.Companion.getUsers(query, threadId, userDatabase);
userSelectionView.show(users, threadId);
}
}
}
}
}
private void clearMentions() {
oldText = "";
currentMentionStartIndex = -1;
mentions.clear();
}
@Override @Override
public void setThreadId(long threadId) { public void setThreadId(long threadId) {
this.threadId = threadId; this.threadId = threadId;
@ -2747,11 +2835,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
inputPanel.setQuote(GlideApp.with(this), inputPanel.setQuote(GlideApp.with(this),
messageRecord.getDateSent(), messageRecord.getDateSent(),
author, author,
body, body,
slideDeck, slideDeck,
recipient); recipient);
} else if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) { } else if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) {
LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0); LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0);
@ -2762,18 +2850,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
inputPanel.setQuote(GlideApp.with(this), inputPanel.setQuote(GlideApp.with(this),
messageRecord.getDateSent(), messageRecord.getDateSent(),
author, author,
messageRecord.getBody(), messageRecord.getBody(),
slideDeck, slideDeck,
recipient); recipient);
} else { } else {
inputPanel.setQuote(GlideApp.with(this), inputPanel.setQuote(GlideApp.with(this),
messageRecord.getDateSent(), messageRecord.getDateSent(),
author, author,
messageRecord.getBody(), messageRecord.getBody(),
messageRecord.isMms() ? ((MmsMessageRecord) messageRecord).getSlideDeck() : new SlideDeck(), messageRecord.isMms() ? ((MmsMessageRecord) messageRecord).getSlideDeck() : new SlideDeck(),
recipient); recipient);
} }
} }

View File

@ -91,6 +91,7 @@ import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.FriendRequestView; import org.thoughtcrime.securesms.loki.FriendRequestView;
import org.thoughtcrime.securesms.loki.FriendRequestViewDelegate; import org.thoughtcrime.securesms.loki.FriendRequestViewDelegate;
import org.thoughtcrime.securesms.loki.LokiMessageDatabase; import org.thoughtcrime.securesms.loki.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.MentionUtilities;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide; import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.PartAuthority;
@ -212,7 +213,7 @@ public class ConversationItem extends LinearLayout
this.groupSenderProfileName = findViewById(R.id.group_message_sender_profile); this.groupSenderProfileName = findViewById(R.id.group_message_sender_profile);
this.alertView = findViewById(R.id.indicators_parent); this.alertView = findViewById(R.id.indicators_parent);
this.contactPhoto = findViewById(R.id.contact_photo); this.contactPhoto = findViewById(R.id.contact_photo);
this.moderatorIconImageView = findViewById(R.id.moderator_icon_image_view); this.moderatorIconImageView = findViewById(R.id.moderator_icon_image_view);
this.contactPhotoHolder = findViewById(R.id.contact_photo_container); this.contactPhotoHolder = findViewById(R.id.contact_photo_container);
this.bodyBubble = findViewById(R.id.body_bubble); this.bodyBubble = findViewById(R.id.body_bubble);
this.mediaThumbnailStub = new Stub<>(findViewById(R.id.image_view_stub)); this.mediaThumbnailStub = new Stub<>(findViewById(R.id.image_view_stub));
@ -260,7 +261,7 @@ public class ConversationItem extends LinearLayout
setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread); setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, conversationRecipient, groupThread); setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, conversationRecipient, groupThread);
setInteractionState(messageRecord, pulseHighlight); setInteractionState(messageRecord, pulseHighlight);
setBodyText(messageRecord, searchQuery); setBodyText(messageRecord, searchQuery, groupThread);
setBubbleState(messageRecord); setBubbleState(messageRecord);
setStatusIcons(messageRecord); setStatusIcons(messageRecord);
setContactPhoto(recipient); setContactPhoto(recipient);
@ -466,17 +467,17 @@ public class ConversationItem extends LinearLayout
!StickerUrl.isValidShareLink(linkPreview.getUrl()); !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.setClickable(false);
bodyText.setFocusable(false); bodyText.setFocusable(false);
bodyText.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(context)); bodyText.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(context));
if (isCaptionlessMms(messageRecord)) { if (isCaptionlessMms(messageRecord)) {
bodyText.setVisibility(View.GONE); bodyText.setVisibility(View.GONE);
} else { } else { ;
Spannable styledText = linkifyMessageBody(messageRecord.getDisplayBody(getContext()), batchSelected.isEmpty()); Spannable text = MentionUtilities.highlightMentions(linkifyMessageBody(messageRecord.getDisplayBody(context), batchSelected.isEmpty()), messageRecord.isOutgoing(), isGroupThread, context);
styledText = SearchUtil.getHighlightedSpan(locale, () -> new BackgroundColorSpan(Color.YELLOW), styledText, searchQuery); text = SearchUtil.getHighlightedSpan(locale, () -> new BackgroundColorSpan(Color.YELLOW), text, searchQuery);
styledText = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), styledText, searchQuery); text = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), text, searchQuery);
if (hasExtraText(messageRecord)) { if (hasExtraText(messageRecord)) {
bodyText.setOverflowText(getLongMessageSpan(messageRecord)); bodyText.setOverflowText(getLongMessageSpan(messageRecord));
@ -484,7 +485,7 @@ public class ConversationItem extends LinearLayout
bodyText.setOverflowText(null); bodyText.setOverflowText(null);
} }
bodyText.setText(styledText); bodyText.setText(text);
bodyText.setVisibility(View.VISIBLE); bodyText.setVisibility(View.VISIBLE);
} }
} }
@ -790,7 +791,8 @@ public class ConversationItem extends LinearLayout
if (current.isMms() && !current.isMmsNotification() && ((MediaMmsMessageRecord)current).getQuote() != null) { if (current.isMms() && !current.isMmsNotification() && ((MediaMmsMessageRecord)current).getQuote() != null) {
Quote quote = ((MediaMmsMessageRecord)current).getQuote(); Quote quote = ((MediaMmsMessageRecord)current).getQuote();
//noinspection ConstantConditions //noinspection ConstantConditions
quoteView.setQuote(glideRequests, quote.getId(), Recipient.from(context, quote.getAuthor(), true), quote.getText(), quote.isOriginalMissing(), quote.getAttachment(), conversationRecipient); String quoteBody = MentionUtilities.highlightMentions(quote.getText(), isGroupThread, context);
quoteView.setQuote(glideRequests, quote.getId(), Recipient.from(context, quote.getAuthor(), true), quoteBody, quote.isOriginalMissing(), quote.getAttachment(), conversationRecipient);
quoteView.setVisibility(View.VISIBLE); quoteView.setVisibility(View.VISIBLE);
quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT; quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;

View File

@ -67,6 +67,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.LokiAPIUtilities;
import org.thoughtcrime.securesms.loki.LokiMessageDatabase; import org.thoughtcrime.securesms.loki.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.LokiPreKeyBundleDatabase; import org.thoughtcrime.securesms.loki.LokiPreKeyBundleDatabase;
import org.thoughtcrime.securesms.loki.LokiPreKeyRecordDatabase; 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.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.loki.api.DeviceLinkingSession; 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.LokiStorageAPI;
import org.whispersystems.signalservice.loki.api.PairingAuthorisation; import org.whispersystems.signalservice.loki.api.PairingAuthorisation;
import org.whispersystems.signalservice.loki.crypto.LokiServiceCipher; 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()); 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 // Loki - Store message server ID
updateGroupChatMessageServerID(messageServerIDOrNull, insertResult); updateGroupChatMessageServerID(messageServerIDOrNull, insertResult);

View File

@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.loki
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import kotlinx.android.synthetic.main.activity_account_details.* import kotlinx.android.synthetic.main.activity_display_name.*
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.BaseActionBarActivity
@ -19,21 +19,21 @@ class DisplayNameActivity : BaseActionBarActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_account_details) setContentView(R.layout.activity_display_name)
nextButton.setOnClickListener { continueIfPossible() } nextButton.setOnClickListener { continueIfPossible() }
Analytics.shared.track("Display Name Screen Viewed") Analytics.shared.track("Display Name Screen Viewed")
} }
private fun continueIfPossible() { private fun continueIfPossible() {
val uncheckedName = nameEditText.text.toString() val name = nameEditText.text.toString()
val name = if (uncheckedName.isNotEmpty()) { uncheckedName.trim() } else { null } if (name.isEmpty()) {
if (name != null) { return nameEditText.input.setError("Invalid")
if (name.toByteArray().size > ProfileCipher.NAME_PADDED_LENGTH) { }
return nameEditText.input.setError("Too Long") if (name.toByteArray().size > ProfileCipher.NAME_PADDED_LENGTH) {
} else { return nameEditText.input.setError("Too Long")
Analytics.shared.track("Display Name Updated") } else {
TextSecurePreferences.setProfileName(this, name) Analytics.shared.track("Display Name Updated")
} TextSecurePreferences.setProfileName(this, name)
} }
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(nameEditText.windowToken, 0) inputMethodManager.hideSoftInputFromWindow(nameEditText.windowToken, 0)

View File

@ -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<String>()
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
}
}

View File

@ -46,7 +46,7 @@ class LokiUserDatabase(context: Context, helper: SQLCipherOpenHelper) : Database
Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false).notifyListeners() 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 val database = databaseHelper.readableDatabase
return database.get(serverDisplayNameTable, "${Companion.hexEncodedPublicKey} = ? AND ${Companion.serverID} = ?", arrayOf( hexEncodedPublicKey, serverID )) { cursor -> return database.get(serverDisplayNameTable, "${Companion.hexEncodedPublicKey} = ? AND ${Companion.serverID} = ?", arrayOf( hexEncodedPublicKey, serverID )) { cursor ->
cursor.getString(cursor.getColumnIndexOrThrow(displayName)) cursor.getString(cursor.getColumnIndexOrThrow(displayName))

View File

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

View File

@ -0,0 +1,56 @@
package org.thoughtcrime.securesms.loki
import android.content.Context
import android.text.Spannable
import android.text.SpannableString
import android.text.style.BackgroundColorSpan
import android.util.Range
import network.loki.messenger.R
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI
import java.util.regex.Pattern
object MentionUtilities {
@JvmStatic
fun highlightMentions(text: CharSequence, isGroupThread: Boolean, context: Context): String {
return MentionUtilities.highlightMentions(text, false, isGroupThread, context).toString() // isOutgoingMessage is irrelevant
}
@JvmStatic
fun highlightMentions(text: CharSequence, isOutgoingMessage: Boolean, isGroupThread: Boolean, context: Context): SpannableString {
var text = text
val pattern = Pattern.compile("@[0-9a-fA-F]*")
var matcher = pattern.matcher(text)
val mentions = mutableListOf<Range<Int>>()
var startIndex = 0
if (matcher.find(startIndex) && isGroupThread) {
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()) {
TextSecurePreferences.getProfileName(context)
} else {
val publicChatID = LokiGroupChatAPI.publicChatServer + "." + LokiGroupChatAPI.publicChatServerID
DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChatID, userID)
}
if (userDisplayName != null) {
text = text.subSequence(0, matcher.start()).toString() + "@" + userDisplayName + text.subSequence(matcher.end(), text.length)
val endIndex = matcher.start() + 1 + userDisplayName.length
startIndex = endIndex
mentions.add(Range.create(matcher.start(), endIndex))
} else {
startIndex = matcher.end()
}
matcher = pattern.matcher(text)
if (!matcher.find(startIndex)) { break }
}
}
val result = SpannableString(text)
for (range in mentions) {
val highlightColor = if (isOutgoingMessage) context.resources.getColor(R.color.loki_dark_green) else context.resources.getColor(R.color.loki_green)
result.setSpan(BackgroundColorSpan(highlightColor), range.lower, range.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
return result
}
}

View File

@ -8,7 +8,7 @@ import android.view.ViewGroup
import kotlinx.android.synthetic.main.fragment_new_conversation.* import kotlinx.android.synthetic.main.fragment_new_conversation.*
import network.loki.messenger.R import network.loki.messenger.R
class NewConversationFragment() : Fragment() { class NewConversationFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_new_conversation, container, false) return inflater.inflate(R.layout.fragment_new_conversation, container, false)

View File

@ -9,9 +9,6 @@ import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import kotlinx.android.synthetic.main.activity_seed.* 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 network.loki.messenger.R
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.BaseActionBarActivity
@ -180,13 +177,12 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
IdentityKeyUtil.generateIdentityKeyPair(this, seed) IdentityKeyUtil.generateIdentityKeyPair(this, seed)
} }
val keyPair = IdentityKeyUtil.getIdentityKeyPair(this) val keyPair = IdentityKeyUtil.getIdentityKeyPair(this)
val publicKey = keyPair.publicKey val userHexEncodedPublicKey = keyPair.hexEncodedPublicKey
val hexEncodedPublicKey = keyPair.hexEncodedPublicKey
val registrationID = KeyHelper.generateRegistrationId(false) val registrationID = KeyHelper.generateRegistrationId(false)
TextSecurePreferences.setLocalRegistrationId(this, registrationID) 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) IdentityDatabase.VerifiedStatus.VERIFIED, true, System.currentTimeMillis(), true)
TextSecurePreferences.setLocalNumber(this, hexEncodedPublicKey) TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey)
when (mode) { when (mode) {
Mode.Register -> Analytics.shared.track("Seed Created") Mode.Register -> Analytics.shared.track("Seed Created")
Mode.Restore -> Analytics.shared.track("Seed Restored") Mode.Restore -> Analytics.shared.track("Seed Restored")
@ -195,22 +191,20 @@ class SeedActivity : BaseActionBarActivity(), DeviceLinkingDialogDelegate {
if (mode == Mode.Link) { if (mode == Mode.Link) {
TextSecurePreferences.setHasSeenWelcomeScreen(this, true) TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
TextSecurePreferences.setPromptedPushRegistration(this, true) TextSecurePreferences.setPromptedPushRegistration(this, true)
val primaryDevicePublicKey = publicKeyEditText.text.trim().toString() val masterHexEncodedPublicKey = publicKeyEditText.text.trim().toString()
val authorisation = PairingAuthorisation(primaryDevicePublicKey, hexEncodedPublicKey).sign(PairingAuthorisation.Type.REQUEST, keyPair.privateKey.serialize()) val authorisation = PairingAuthorisation(masterHexEncodedPublicKey, userHexEncodedPublicKey).sign(PairingAuthorisation.Type.REQUEST, keyPair.privateKey.serialize())
if (authorisation == null) { if (authorisation == null) {
Log.d("Loki", "Failed to sign outgoing pairing request.") Log.d("Loki", "Failed to sign pairing request.")
resetForRegistration() 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) val application = ApplicationContext.getInstance(this)
application.startLongPollingIfNeeded() application.startLongPollingIfNeeded()
application.setUpP2PAPI() application.setUpP2PAPI()
application.setUpStorageAPIIfNeeded() application.setUpStorageAPIIfNeeded()
DeviceLinkingDialog.show(this, DeviceLinkingView.Mode.Slave, this) DeviceLinkingDialog.show(this, DeviceLinkingView.Mode.Slave, this)
CoroutineScope(Dispatchers.Main).launch { retryIfNeeded(8) {
retryIfNeeded(8) { sendPairingAuthorisationMessage(this@SeedActivity, authorisation.primaryDevicePublicKey, authorisation).get()
sendPairingAuthorisationMessage(this@SeedActivity, authorisation.primaryDevicePublicKey, authorisation).get()
}
} }
} else { } else {
startActivity(Intent(this, DisplayNameActivity::class.java)) startActivity(Intent(this, DisplayNameActivity::class.java))

View File

@ -0,0 +1,71 @@
package org.thoughtcrime.securesms.loki
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
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
class UserSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) {
private var users = listOf<Tuple2<String, String>>()
set(newValue) { field = newValue; userSelectionViewAdapter.users = newValue }
private var hasGroupContext = false
var onUserSelected: ((Tuple2<String, String>) -> Unit)? = null
private val userSelectionViewAdapter by lazy { Adapter(context) }
private class Adapter(private val context: Context) : BaseAdapter() {
var users = listOf<Tuple2<String, String>>()
set(newValue) { field = newValue; notifyDataSetChanged() }
var hasGroupContext = false
override fun getCount(): Int {
return users.count()
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getItem(position: Int): Tuple2<String, String> {
return users[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
cell.hasGroupContext = hasGroupContext
return cell
}
}
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context) : this(context, null)
init {
adapter = userSelectionViewAdapter
userSelectionViewAdapter.users = users
setOnItemClickListener { _, _, position, _ ->
onUserSelected?.invoke(users[position])
}
}
fun show(users: List<Tuple2<String, String>>, threadID: Long) {
hasGroupContext = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadID)!!.isGroupRecipient
this.users = users
val layoutParams = this.layoutParams as ViewGroup.LayoutParams
layoutParams.height = toPx(6 + Math.min(users.count(), 4) * 52, resources)
this.layoutParams = layoutParams
}
fun hide() {
val layoutParams = this.layoutParams as ViewGroup.LayoutParams
layoutParams.height = 0
this.layoutParams = layoutParams
}
}

View File

@ -0,0 +1,48 @@
package org.thoughtcrime.securesms.loki
import android.content.Context
import android.graphics.Outline
import android.util.AttributeSet
import android.view.LayoutInflater
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 network.loki.messenger.R
import nl.komponents.kovenant.combine.Tuple2
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI
class UserSelectionViewCell(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {
var user = Tuple2("", "")
set(newValue) { field = newValue; update() }
var hasGroupContext = false
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context) : this(context, null)
companion object {
fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): UserSelectionViewCell {
return layoutInflater.inflate(R.layout.cell_user_selection_view, parent, false) as UserSelectionViewCell
}
}
override fun onFinishInflate() {
super.onFinishInflate()
profilePictureImageViewContainer.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setOval(0, 0, view.width, view.height)
}
}
profilePictureImageViewContainer.clipToOutline = true
}
private fun update() {
displayNameTextView.text = user.second
profilePictureImageView.update(user.first)
val isUserModerator = LokiGroupChatAPI.isUserModerator(user.first, LokiGroupChatAPI.publicChatServerID, LokiGroupChatAPI.publicChatServer)
moderatorIconImageView.visibility = if (isUserModerator && hasGroupContext) View.VISIBLE else View.GONE
}
}

View File

@ -100,7 +100,7 @@ public class ProfilePreference extends Preference {
int height = avatarView.getHeight(); int height = avatarView.getHeight();
if (width == 0 || height == 0) return true; if (width == 0 || height == 0) return true;
avatarView.getViewTreeObserver().removeOnPreDrawListener(this); avatarView.getViewTreeObserver().removeOnPreDrawListener(this);
JazzIdenticonDrawable identicon = new JazzIdenticonDrawable(width, height, userHexEncodedPublicKey); JazzIdenticonDrawable identicon = new JazzIdenticonDrawable(width, height, userHexEncodedPublicKey.toLowerCase());
avatarView.setImageDrawable(identicon); avatarView.setImageDrawable(identicon);
return true; return true;
} }