mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-23 12:46:33 +00:00
51
res/layout/cell_user_selection_view.xml
Normal file
51
res/layout/cell_user_selection_view.xml
Normal 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>
|
||||||
@@ -71,6 +71,13 @@
|
|||||||
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" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<include layout="@layout/view_user_selection" />
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -83,6 +90,8 @@
|
|||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/register_button"
|
android:id="@+id/register_button"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
8
res/layout/view_user_selection.xml
Normal file
8
res/layout/view_user_selection.xml
Normal 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" />
|
||||||
@@ -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 -->
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -123,6 +124,10 @@ public class AvatarImageView extends AppCompatImageView {
|
|||||||
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;
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -156,7 +156,10 @@ 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.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,6 +227,7 @@ 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;
|
||||||
@@ -239,7 +243,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;
|
||||||
@@ -309,6 +315,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
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 +345,8 @@ 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();
|
||||||
|
|
||||||
|
private UserSelectionView userSelectionView;
|
||||||
|
private int mentionStartIndex = -1;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPreCreate() {
|
protected void onPreCreate() {
|
||||||
@@ -389,11 +398,21 @@ 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 -> {
|
||||||
|
String oldText = composeText.getText().toString();
|
||||||
|
String newText = oldText.substring(0, mentionStartIndex) + tuple.getFirst();
|
||||||
|
composeText.setText(newText);
|
||||||
|
composeText.setSelection(newText.length());
|
||||||
|
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 +1571,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 +1584,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();
|
||||||
@@ -2591,7 +2612,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
|
|
||||||
private void silentlySetComposeText(String text) {
|
private void silentlySetComposeText(String text) {
|
||||||
typingTextWatcher.setEnabled(false);
|
typingTextWatcher.setEnabled(false);
|
||||||
|
mentionTextWatcher.setEnabled(false);
|
||||||
composeText.setText(text);
|
composeText.setText(text);
|
||||||
|
mentionTextWatcher.setEnabled(true);
|
||||||
typingTextWatcher.setEnabled(true);
|
typingTextWatcher.setEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2706,7 +2729,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 +2743,36 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class MentionTextWatcher extends SimpleTextWatcher {
|
||||||
|
private boolean enabled = true;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(String text) {
|
||||||
|
if (!enabled | text.length() == 0) { return; }
|
||||||
|
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);
|
||||||
|
mentionStartIndex = currentEndIndex + 1;
|
||||||
|
userSelectionView.show(users, threadId);
|
||||||
|
} else if (Character.getType(lastCharacter) != Character.UPPERCASE_LETTER && Character.getType(lastCharacter) != Character.LOWERCASE_LETTER) {
|
||||||
|
mentionStartIndex = -1;
|
||||||
|
userSelectionView.hide();
|
||||||
|
} else {
|
||||||
|
if (mentionStartIndex != -1) {
|
||||||
|
String query = text.substring(mentionStartIndex);
|
||||||
|
List<Tuple2<String, String>> users = LokiAPI.Companion.getUsers(query, threadId, userDatabase);
|
||||||
|
userSelectionView.show(users, threadId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setThreadId(long threadId) {
|
public void setThreadId(long threadId) {
|
||||||
this.threadId = threadId;
|
this.threadId = threadId;
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import android.text.style.ForegroundColorSpan;
|
|||||||
import android.text.style.URLSpan;
|
import android.text.style.URLSpan;
|
||||||
import android.text.util.Linkify;
|
import android.text.util.Linkify;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Range;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
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.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI;
|
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import network.loki.messenger.R;
|
import network.loki.messenger.R;
|
||||||
|
|
||||||
@@ -258,7 +262,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);
|
||||||
@@ -464,7 +468,7 @@ 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));
|
||||||
@@ -472,9 +476,9 @@ public class ConversationItem extends LinearLayout
|
|||||||
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 = linkifyMessageBody(highlightMentions(messageRecord.getDisplayBody(context), isGroupThread), batchSelected.isEmpty());
|
||||||
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));
|
||||||
@@ -482,7 +486,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -772,6 +776,40 @@ public class ConversationItem extends LinearLayout
|
|||||||
return messageBody;
|
return messageBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SpannableString highlightMentions(CharSequence text, boolean isGroupThread) {
|
||||||
|
Pattern pattern = Pattern.compile("@\\w*");
|
||||||
|
Matcher matcher = pattern.matcher(text);
|
||||||
|
ArrayList<Range<Integer>> 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<Integer> 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) {
|
private void setStatusIcons(MessageRecord messageRecord) {
|
||||||
bodyText.setCompoundDrawablesWithIntrinsicBounds(0, 0, messageRecord.isKeyExchange() ? R.drawable.ic_menu_login : 0, 0);
|
bodyText.setCompoundDrawablesWithIntrinsicBounds(0, 0, messageRecord.isKeyExchange() ? R.drawable.ic_menu_login : 0, 0);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
29
src/org/thoughtcrime/securesms/loki/LokiAPIUtilities.kt
Normal file
29
src/org/thoughtcrime/securesms/loki/LokiAPIUtilities.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
|||||||
@@ -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,23 +191,21 @@ 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))
|
||||||
finish()
|
finish()
|
||||||
|
|||||||
71
src/org/thoughtcrime/securesms/loki/UserSelectionView.kt
Normal file
71
src/org/thoughtcrime/securesms/loki/UserSelectionView.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/org/thoughtcrime/securesms/loki/UserSelectionViewCell.kt
Normal file
48
src/org/thoughtcrime/securesms/loki/UserSelectionViewCell.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user