Add inline emoji search.
This commit is contained in:
parent
ba7319e215
commit
19af68a27c
22 changed files with 708 additions and 93 deletions
|
@ -26,6 +26,7 @@ import androidx.core.view.inputmethod.EditorInfoCompat;
|
||||||
import androidx.core.view.inputmethod.InputConnectionCompat;
|
import androidx.core.view.inputmethod.InputConnectionCompat;
|
||||||
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
||||||
|
|
||||||
|
import org.signal.core.util.StringUtil;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
|
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
|
||||||
|
@ -34,19 +35,25 @@ import org.thoughtcrime.securesms.components.mention.MentionDeleter;
|
||||||
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
||||||
import org.thoughtcrime.securesms.components.mention.MentionValidatorWatcher;
|
import org.thoughtcrime.securesms.components.mention.MentionValidatorWatcher;
|
||||||
import org.thoughtcrime.securesms.conversation.MessageSendType;
|
import org.thoughtcrime.securesms.conversation.MessageSendType;
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQuery;
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryChangedListener;
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryReplacement;
|
||||||
import org.thoughtcrime.securesms.database.model.Mention;
|
import org.thoughtcrime.securesms.database.model.Mention;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.signal.core.util.StringUtil;
|
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static org.thoughtcrime.securesms.database.MentionUtil.MENTION_STARTER;
|
import static org.thoughtcrime.securesms.database.MentionUtil.MENTION_STARTER;
|
||||||
|
|
||||||
public class ComposeText extends EmojiEditText {
|
public class ComposeText extends EmojiEditText {
|
||||||
|
|
||||||
|
private static final char EMOJI_STARTER = ':';
|
||||||
|
private static final long EMOJI_KEYWORD_DELAY = TimeUnit.SECONDS.toMillis(1);
|
||||||
|
|
||||||
private CharSequence hint;
|
private CharSequence hint;
|
||||||
private SpannableString subHint;
|
private SpannableString subHint;
|
||||||
private MentionRendererDelegate mentionRendererDelegate;
|
private MentionRendererDelegate mentionRendererDelegate;
|
||||||
|
@ -54,7 +61,14 @@ public class ComposeText extends EmojiEditText {
|
||||||
|
|
||||||
@Nullable private InputPanel.MediaListener mediaListener;
|
@Nullable private InputPanel.MediaListener mediaListener;
|
||||||
@Nullable private CursorPositionChangedListener cursorPositionChangedListener;
|
@Nullable private CursorPositionChangedListener cursorPositionChangedListener;
|
||||||
@Nullable private MentionQueryChangedListener mentionQueryChangedListener;
|
@Nullable private InlineQueryChangedListener inlineQueryChangedListener;
|
||||||
|
|
||||||
|
private final Runnable keywordSearchRunnable = () -> {
|
||||||
|
Editable text = getText();
|
||||||
|
if (text != null && enoughToFilter(text, true)) {
|
||||||
|
performFiltering(text, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public ComposeText(Context context) {
|
public ComposeText(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
|
@ -111,7 +125,7 @@ public class ComposeText extends EmojiEditText {
|
||||||
if (selectionStart == selectionEnd) {
|
if (selectionStart == selectionEnd) {
|
||||||
doAfterCursorChange(getText());
|
doAfterCursorChange(getText());
|
||||||
} else {
|
} else {
|
||||||
updateQuery(null);
|
clearInlineQuery();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,8 +203,8 @@ public class ComposeText extends EmojiEditText {
|
||||||
this.cursorPositionChangedListener = listener;
|
this.cursorPositionChangedListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMentionQueryChangedListener(@Nullable MentionQueryChangedListener listener) {
|
public void setInlineQueryChangedListener(@Nullable InlineQueryChangedListener listener) {
|
||||||
this.mentionQueryChangedListener = listener;
|
this.inlineQueryChangedListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMentionValidator(@Nullable MentionValidatorWatcher.MentionValidator mentionValidator) {
|
public void setMentionValidator(@Nullable MentionValidatorWatcher.MentionValidator mentionValidator) {
|
||||||
|
@ -226,15 +240,23 @@ public class ComposeText extends EmojiEditText {
|
||||||
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
|
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
|
||||||
InputConnection inputConnection = super.onCreateInputConnection(editorInfo);
|
InputConnection inputConnection = super.onCreateInputConnection(editorInfo);
|
||||||
|
|
||||||
if(SignalStore.settings().isEnterKeySends()) {
|
if (SignalStore.settings().isEnterKeySends()) {
|
||||||
editorInfo.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
|
editorInfo.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < 21) return inputConnection;
|
if (Build.VERSION.SDK_INT < 21) {
|
||||||
if (mediaListener == null) return inputConnection;
|
return inputConnection;
|
||||||
if (inputConnection == null) return null;
|
}
|
||||||
|
|
||||||
EditorInfoCompat.setContentMimeTypes(editorInfo, new String[] {"image/jpeg", "image/png", "image/gif"});
|
if (mediaListener == null) {
|
||||||
|
return inputConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputConnection == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorInfoCompat.setContentMimeTypes(editorInfo, new String[] { "image/jpeg", "image/png", "image/gif" });
|
||||||
return InputConnectionCompat.createWrapper(inputConnection, editorInfo, new CommitContentListener(mediaListener));
|
return InputConnectionCompat.createWrapper(inputConnection, editorInfo, new CommitContentListener(mediaListener));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,35 +322,53 @@ public class ComposeText extends EmojiEditText {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doAfterCursorChange(@NonNull Editable text) {
|
private void doAfterCursorChange(@NonNull Editable text) {
|
||||||
if (enoughToFilter(text)) {
|
removeCallbacks(keywordSearchRunnable);
|
||||||
performFiltering(text);
|
if (enoughToFilter(text, false)) {
|
||||||
|
performFiltering(text, false);
|
||||||
} else {
|
} else {
|
||||||
updateQuery(null);
|
postDelayed(keywordSearchRunnable, EMOJI_KEYWORD_DELAY);
|
||||||
|
clearInlineQuery();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void performFiltering(@NonNull Editable text) {
|
private void performFiltering(@NonNull Editable text, boolean keywordEmojiSearch) {
|
||||||
int end = getSelectionEnd();
|
int end = getSelectionEnd();
|
||||||
int start = findQueryStart(text, end);
|
QueryStart queryStart = findQueryStart(text, end, keywordEmojiSearch);
|
||||||
CharSequence query = text.subSequence(start, end);
|
int start = queryStart.index;
|
||||||
updateQuery(query.toString());
|
String query = text.subSequence(start, end).toString();
|
||||||
}
|
|
||||||
|
|
||||||
private void updateQuery(@Nullable String query) {
|
if (inlineQueryChangedListener != null) {
|
||||||
if (mentionQueryChangedListener != null) {
|
if (queryStart.isMentionQuery) {
|
||||||
mentionQueryChangedListener.onQueryChanged(query);
|
inlineQueryChangedListener.onQueryChanged(new InlineQuery.Mention(query));
|
||||||
|
} else {
|
||||||
|
inlineQueryChangedListener.onQueryChanged(new InlineQuery.Emoji(query, keywordEmojiSearch));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean enoughToFilter(@NonNull Editable text) {
|
private void clearInlineQuery() {
|
||||||
|
if (inlineQueryChangedListener != null) {
|
||||||
|
inlineQueryChangedListener.clearQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean enoughToFilter(@NonNull Editable text, boolean keywordEmojiSearch) {
|
||||||
int end = getSelectionEnd();
|
int end = getSelectionEnd();
|
||||||
if (end < 0) {
|
if (end < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return findQueryStart(text, end) != -1;
|
return findQueryStart(text, end, keywordEmojiSearch).index != -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void replaceTextWithMention(@NonNull String displayName, @NonNull RecipientId recipientId) {
|
public void replaceTextWithMention(@NonNull String displayName, @NonNull RecipientId recipientId) {
|
||||||
|
replaceText(createReplacementToken(displayName, recipientId), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void replaceText(@NonNull InlineQueryReplacement replacement) {
|
||||||
|
replaceText(replacement.toCharSequence(getContext()), replacement.isKeywordSearch());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void replaceText(@NonNull CharSequence replacement, boolean keywordReplacement) {
|
||||||
Editable text = getText();
|
Editable text = getText();
|
||||||
if (text == null) {
|
if (text == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -336,10 +376,11 @@ public class ComposeText extends EmojiEditText {
|
||||||
|
|
||||||
clearComposingText();
|
clearComposingText();
|
||||||
|
|
||||||
int end = getSelectionEnd();
|
int end = getSelectionEnd();
|
||||||
int start = findQueryStart(text, end) - 1;
|
int start = findQueryStart(text, end, keywordReplacement).index - (keywordReplacement ? 0 : 1);
|
||||||
|
|
||||||
text.replace(start, end, createReplacementToken(displayName, recipientId));
|
text.replace(start, end, "");
|
||||||
|
text.insert(start, replacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NonNull CharSequence createReplacementToken(@NonNull CharSequence text, @NonNull RecipientId recipientId) {
|
private @NonNull CharSequence createReplacementToken(@NonNull CharSequence text, @NonNull RecipientId recipientId) {
|
||||||
|
@ -357,17 +398,37 @@ public class ComposeText extends EmojiEditText {
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int findQueryStart(@NonNull CharSequence text, int inputCursorPosition) {
|
private QueryStart findQueryStart(@NonNull CharSequence text, int inputCursorPosition, boolean keywordEmojiSearch) {
|
||||||
|
if (keywordEmojiSearch) {
|
||||||
|
int start = findQueryStart(text, inputCursorPosition, ' ');
|
||||||
|
if (start == -1 && inputCursorPosition != 0) {
|
||||||
|
start = 0;
|
||||||
|
} else if (start == inputCursorPosition) {
|
||||||
|
start = -1;
|
||||||
|
}
|
||||||
|
return new QueryStart(start, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryStart queryStart = new QueryStart(findQueryStart(text, inputCursorPosition, MENTION_STARTER), true);
|
||||||
|
|
||||||
|
if (queryStart.index < 0) {
|
||||||
|
queryStart = new QueryStart(findQueryStart(text, inputCursorPosition, EMOJI_STARTER), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int findQueryStart(@NonNull CharSequence text, int inputCursorPosition, char starter) {
|
||||||
if (inputCursorPosition == 0) {
|
if (inputCursorPosition == 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int delimiterSearchIndex = inputCursorPosition - 1;
|
int delimiterSearchIndex = inputCursorPosition - 1;
|
||||||
while (delimiterSearchIndex >= 0 && (text.charAt(delimiterSearchIndex) != MENTION_STARTER && text.charAt(delimiterSearchIndex) != ' ')) {
|
while (delimiterSearchIndex >= 0 && (text.charAt(delimiterSearchIndex) != starter && text.charAt(delimiterSearchIndex) != ' ')) {
|
||||||
delimiterSearchIndex--;
|
delimiterSearchIndex--;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (delimiterSearchIndex >= 0 && text.charAt(delimiterSearchIndex) == MENTION_STARTER) {
|
if (delimiterSearchIndex >= 0 && text.charAt(delimiterSearchIndex) == starter) {
|
||||||
return delimiterSearchIndex + 1;
|
return delimiterSearchIndex + 1;
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -405,11 +466,18 @@ public class ComposeText extends EmojiEditText {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class QueryStart {
|
||||||
|
public int index;
|
||||||
|
public boolean isMentionQuery;
|
||||||
|
|
||||||
|
public QueryStart(int index, boolean isMentionQuery) {
|
||||||
|
this.index = index;
|
||||||
|
this.isMentionQuery = isMentionQuery;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public interface CursorPositionChangedListener {
|
public interface CursorPositionChangedListener {
|
||||||
void onCursorPositionChanged(int start, int end);
|
void onCursorPositionChanged(int start, int end);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface MentionQueryChangedListener {
|
|
||||||
void onQueryChanged(@Nullable String query);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,14 +10,17 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.widget.AppCompatEditText;
|
import androidx.appcompat.widget.AppCompatEditText;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
|
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
|
||||||
public class EmojiEditText extends AppCompatEditText {
|
public class EmojiEditText extends AppCompatEditText {
|
||||||
private static final String TAG = Log.tag(EmojiEditText.class);
|
|
||||||
|
private final Set<OnFocusChangeListener> onFocusChangeListeners = new HashSet<>();
|
||||||
|
|
||||||
public EmojiEditText(Context context) {
|
public EmojiEditText(Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
|
@ -38,6 +41,12 @@ public class EmojiEditText extends AppCompatEditText {
|
||||||
if (!isInEditMode() && (forceCustom || !SignalStore.settings().isPreferSystemEmoji())) {
|
if (!isInEditMode() && (forceCustom || !SignalStore.settings().isPreferSystemEmoji())) {
|
||||||
setFilters(appendEmojiFilter(this.getFilters(), jumboEmoji));
|
setFilters(appendEmojiFilter(this.getFilters(), jumboEmoji));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
super.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
for (OnFocusChangeListener listener : onFocusChangeListeners) {
|
||||||
|
listener.onFocusChange(v, hasFocus);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void insertEmoji(String emoji) {
|
public void insertEmoji(String emoji) {
|
||||||
|
@ -54,6 +63,17 @@ public class EmojiEditText extends AppCompatEditText {
|
||||||
else super.invalidateDrawable(drawable);
|
else super.invalidateDrawable(drawable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOnFocusChangeListener(@Nullable OnFocusChangeListener listener) {
|
||||||
|
if (listener != null) {
|
||||||
|
onFocusChangeListeners.add(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addOnFocusChangeListener(@NonNull OnFocusChangeListener listener) {
|
||||||
|
onFocusChangeListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
private InputFilter[] appendEmojiFilter(@Nullable InputFilter[] originalFilters, boolean jumboEmoji) {
|
private InputFilter[] appendEmojiFilter(@Nullable InputFilter[] originalFilters, boolean jumboEmoji) {
|
||||||
InputFilter[] result;
|
InputFilter[] result;
|
||||||
|
|
||||||
|
|
|
@ -133,11 +133,11 @@ class SignalContextMenu private constructor(
|
||||||
val container: ViewGroup
|
val container: ViewGroup
|
||||||
) {
|
) {
|
||||||
|
|
||||||
var onDismiss: Runnable? = null
|
private var onDismiss: Runnable? = null
|
||||||
var offsetX = 0
|
private var offsetX = 0
|
||||||
var offsetY = 0
|
private var offsetY = 0
|
||||||
var horizontalPosition = HorizontalPosition.START
|
private var horizontalPosition = HorizontalPosition.START
|
||||||
var verticalPosition = VerticalPosition.BELOW
|
private var verticalPosition = VerticalPosition.BELOW
|
||||||
|
|
||||||
fun onDismiss(onDismiss: Runnable): Builder {
|
fun onDismiss(onDismiss: Runnable): Builder {
|
||||||
this.onDismiss = onDismiss
|
this.onDismiss = onDismiss
|
||||||
|
|
|
@ -103,6 +103,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
import org.greenrobot.eventbus.EventBus;
|
import org.greenrobot.eventbus.EventBus;
|
||||||
import org.greenrobot.eventbus.Subscribe;
|
import org.greenrobot.eventbus.Subscribe;
|
||||||
import org.greenrobot.eventbus.ThreadMode;
|
import org.greenrobot.eventbus.ThreadMode;
|
||||||
|
import org.signal.core.util.DimensionUnit;
|
||||||
import org.signal.core.util.ThreadUtil;
|
import org.signal.core.util.ThreadUtil;
|
||||||
import org.signal.core.util.concurrent.SignalExecutors;
|
import org.signal.core.util.concurrent.SignalExecutors;
|
||||||
import org.signal.core.util.concurrent.SimpleTask;
|
import org.signal.core.util.concurrent.SimpleTask;
|
||||||
|
@ -137,6 +138,8 @@ import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
|
||||||
import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
|
import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
|
||||||
import org.thoughtcrime.securesms.components.location.SignalPlace;
|
import org.thoughtcrime.securesms.components.location.SignalPlace;
|
||||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||||
|
import org.thoughtcrime.securesms.components.menu.ActionItem;
|
||||||
|
import org.thoughtcrime.securesms.components.menu.SignalContextMenu;
|
||||||
import org.thoughtcrime.securesms.components.reminder.BubbleOptOutReminder;
|
import org.thoughtcrime.securesms.components.reminder.BubbleOptOutReminder;
|
||||||
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
|
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
|
||||||
import org.thoughtcrime.securesms.components.reminder.GroupsV1MigrationSuggestionsReminder;
|
import org.thoughtcrime.securesms.components.reminder.GroupsV1MigrationSuggestionsReminder;
|
||||||
|
@ -163,6 +166,11 @@ import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationM
|
||||||
import org.thoughtcrime.securesms.conversation.drafts.DraftRepository;
|
import org.thoughtcrime.securesms.conversation.drafts.DraftRepository;
|
||||||
import org.thoughtcrime.securesms.conversation.drafts.DraftViewModel;
|
import org.thoughtcrime.securesms.conversation.drafts.DraftViewModel;
|
||||||
import org.thoughtcrime.securesms.conversation.ui.groupcall.GroupCallViewModel;
|
import org.thoughtcrime.securesms.conversation.ui.groupcall.GroupCallViewModel;
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQuery;
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryChangedListener;
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryResultsController;
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryResultsPopup;
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryViewModel;
|
||||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
|
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
|
||||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||||
|
@ -328,8 +336,11 @@ import java.util.concurrent.TimeoutException;
|
||||||
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 io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
|
||||||
|
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
|
||||||
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -443,12 +454,14 @@ public class ConversationParentFragment extends Fragment
|
||||||
private InviteReminderModel inviteReminderModel;
|
private InviteReminderModel inviteReminderModel;
|
||||||
private ConversationGroupViewModel groupViewModel;
|
private ConversationGroupViewModel groupViewModel;
|
||||||
private MentionsPickerViewModel mentionsViewModel;
|
private MentionsPickerViewModel mentionsViewModel;
|
||||||
|
private InlineQueryViewModel inlineQueryViewModel;
|
||||||
private GroupCallViewModel groupCallViewModel;
|
private GroupCallViewModel groupCallViewModel;
|
||||||
private VoiceRecorderWakeLock voiceRecorderWakeLock;
|
private VoiceRecorderWakeLock voiceRecorderWakeLock;
|
||||||
private DraftViewModel draftViewModel;
|
private DraftViewModel draftViewModel;
|
||||||
private VoiceNoteMediaController voiceNoteMediaController;
|
private VoiceNoteMediaController voiceNoteMediaController;
|
||||||
private VoiceNotePlayerView voiceNotePlayerView;
|
private VoiceNotePlayerView voiceNotePlayerView;
|
||||||
private Material3OnScrollHelper material3OnScrollHelper;
|
private Material3OnScrollHelper material3OnScrollHelper;
|
||||||
|
private InlineQueryResultsController inlineQueryResultsController;
|
||||||
|
|
||||||
private LiveRecipient recipient;
|
private LiveRecipient recipient;
|
||||||
private long threadId;
|
private long threadId;
|
||||||
|
@ -644,6 +657,10 @@ public class ConversationParentFragment extends Fragment
|
||||||
if (reactionDelegate.isShowing()) {
|
if (reactionDelegate.isShowing()) {
|
||||||
reactionDelegate.hide();
|
reactionDelegate.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (inlineQueryResultsController != null) {
|
||||||
|
inlineQueryResultsController.onOrientationChange(newConfig.orientation == ORIENTATION_LANDSCAPE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -2330,7 +2347,17 @@ public class ConversationParentFragment extends Fragment
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeMentionsViewModel() {
|
private void initializeMentionsViewModel() {
|
||||||
mentionsViewModel = new ViewModelProvider(requireActivity(), new MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel.class);
|
mentionsViewModel = new ViewModelProvider(requireActivity(), new MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel.class);
|
||||||
|
inlineQueryViewModel = new ViewModelProvider(requireActivity()).get(InlineQueryViewModel.class);
|
||||||
|
|
||||||
|
inlineQueryResultsController = new InlineQueryResultsController(
|
||||||
|
requireContext(),
|
||||||
|
inlineQueryViewModel,
|
||||||
|
inputPanel,
|
||||||
|
(ViewGroup) requireView(),
|
||||||
|
composeText,
|
||||||
|
getViewLifecycleOwner()
|
||||||
|
);
|
||||||
|
|
||||||
recipient.observe(getViewLifecycleOwner(), r -> {
|
recipient.observe(getViewLifecycleOwner(), r -> {
|
||||||
if (r.isPushV2Group() && !mentionsSuggestions.resolved()) {
|
if (r.isPushV2Group() && !mentionsSuggestions.resolved()) {
|
||||||
|
@ -2339,12 +2366,29 @@ public class ConversationParentFragment extends Fragment
|
||||||
mentionsViewModel.onRecipientChange(r);
|
mentionsViewModel.onRecipientChange(r);
|
||||||
});
|
});
|
||||||
|
|
||||||
composeText.setMentionQueryChangedListener(query -> {
|
composeText.setInlineQueryChangedListener(new InlineQueryChangedListener() {
|
||||||
if (getRecipient().isPushV2Group() && getRecipient().isActiveGroup()) {
|
@Override
|
||||||
if (!mentionsSuggestions.resolved()) {
|
public void onQueryChanged(@NonNull InlineQuery inlineQuery) {
|
||||||
mentionsSuggestions.get();
|
if (inlineQuery instanceof InlineQuery.Mention) {
|
||||||
|
if (getRecipient().isPushV2Group() && getRecipient().isActiveGroup()) {
|
||||||
|
if (!mentionsSuggestions.resolved()) {
|
||||||
|
mentionsSuggestions.get();
|
||||||
|
}
|
||||||
|
mentionsViewModel.onQueryChange(inlineQuery.getQuery());
|
||||||
|
}
|
||||||
|
inlineQueryViewModel.onQueryChange(inlineQuery);
|
||||||
|
} else if (inlineQuery instanceof InlineQuery.Emoji) {
|
||||||
|
inlineQueryViewModel.onQueryChange(inlineQuery);
|
||||||
|
mentionsViewModel.onQueryChange(null);
|
||||||
|
} else if (inlineQuery instanceof InlineQuery.NoQuery) {
|
||||||
|
mentionsViewModel.onQueryChange(null);
|
||||||
|
inlineQueryViewModel.onQueryChange(inlineQuery);
|
||||||
}
|
}
|
||||||
mentionsViewModel.onQueryChange(query);
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearQuery() {
|
||||||
|
onQueryChanged(InlineQuery.NoQuery.INSTANCE);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2365,6 +2409,15 @@ public class ConversationParentFragment extends Fragment
|
||||||
mentionsViewModel.getSelectedRecipient().observe(getViewLifecycleOwner(), recipient -> {
|
mentionsViewModel.getSelectedRecipient().observe(getViewLifecycleOwner(), recipient -> {
|
||||||
composeText.replaceTextWithMention(recipient.getDisplayName(requireContext()), recipient.getId());
|
composeText.replaceTextWithMention(recipient.getDisplayName(requireContext()), recipient.getId());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Disposable disposable = inlineQueryViewModel
|
||||||
|
.getSelection()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(r -> {
|
||||||
|
composeText.replaceText(r);
|
||||||
|
});
|
||||||
|
|
||||||
|
disposables.add(disposable);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initializeGroupCallViewModel() {
|
public void initializeGroupCallViewModel() {
|
||||||
|
@ -3776,6 +3829,7 @@ public class ConversationParentFragment extends Fragment
|
||||||
reactionDelegate.setOnActionSelectedListener(onActionSelectedListener);
|
reactionDelegate.setOnActionSelectedListener(onActionSelectedListener);
|
||||||
reactionDelegate.setOnHideListener(onHideListener);
|
reactionDelegate.setOnHideListener(onHideListener);
|
||||||
reactionDelegate.show(requireActivity(), recipient.get(), conversationMessage, groupViewModel.isNonAdminInAnnouncementGroup(), selectedConversationModel);
|
reactionDelegate.show(requireActivity(), recipient.get(), conversationMessage, groupViewModel.isNonAdminInAnnouncementGroup(), selectedConversationModel);
|
||||||
|
composeText.clearFocus();
|
||||||
if (attachmentKeyboardStub.resolved()) {
|
if (attachmentKeyboardStub.resolved()) {
|
||||||
attachmentKeyboardStub.get().hide(true);
|
attachmentKeyboardStub.get().hide(true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package org.thoughtcrime.securesms.conversation.ui.inlinequery
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an inline query via compose text.
|
||||||
|
*/
|
||||||
|
sealed class InlineQuery(val query: String) {
|
||||||
|
object NoQuery : InlineQuery("")
|
||||||
|
class Emoji(query: String, val keywordSearch: Boolean) : InlineQuery(query.replace('_', ' '))
|
||||||
|
class Mention(query: String) : InlineQuery(query)
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.thoughtcrime.securesms.conversation.ui.inlinequery
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.AnyMappingModel
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||||
|
|
||||||
|
class InlineQueryAdapter(listener: (AnyMappingModel) -> Unit) : MappingAdapter() {
|
||||||
|
init {
|
||||||
|
registerFactory(InlineQueryEmojiResult.Model::class.java, { InlineQueryEmojiResult.ViewHolder(it, listener) }, R.layout.inline_query_emoji_result)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.thoughtcrime.securesms.conversation.ui.inlinequery
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a query changes.
|
||||||
|
*/
|
||||||
|
interface InlineQueryChangedListener {
|
||||||
|
fun onQueryChanged(inlineQuery: InlineQuery)
|
||||||
|
fun clearQuery() = onQueryChanged(InlineQuery.NoQuery)
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package org.thoughtcrime.securesms.conversation.ui.inlinequery
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.AnyMappingModel
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to render inline emoji search results in a [org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter]
|
||||||
|
*/
|
||||||
|
object InlineQueryEmojiResult {
|
||||||
|
|
||||||
|
class Model(val canonicalEmoji: String, val preferredEmoji: String, val keywordSearch: Boolean) : MappingModel<Model> {
|
||||||
|
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||||
|
return canonicalEmoji == newItem.canonicalEmoji
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||||
|
return preferredEmoji == newItem.preferredEmoji
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder(itemView: View, private val listener: (AnyMappingModel) -> Unit) : MappingViewHolder<Model>(itemView) {
|
||||||
|
|
||||||
|
private val emoji: EmojiImageView = findViewById(R.id.inline_query_emoji_image)
|
||||||
|
|
||||||
|
override fun bind(model: Model) {
|
||||||
|
itemView.setOnClickListener { listener(model) }
|
||||||
|
emoji.setImageEmoji(model.preferredEmoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package org.thoughtcrime.securesms.conversation.ui.inlinequery
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulate how to replace a query with a user selected result.
|
||||||
|
*/
|
||||||
|
sealed class InlineQueryReplacement(@get:JvmName("isKeywordSearch") val keywordSearch: Boolean = false) {
|
||||||
|
abstract fun toCharSequence(context: Context): CharSequence
|
||||||
|
|
||||||
|
class Emoji(private val emoji: String, keywordSearch: Boolean) : InlineQueryReplacement(keywordSearch) {
|
||||||
|
override fun toCharSequence(context: Context): CharSequence {
|
||||||
|
return "$emoji "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
package org.thoughtcrime.securesms.conversation.ui.inlinequery
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
|
import org.signal.core.util.DimensionUnit
|
||||||
|
import org.thoughtcrime.securesms.components.ComposeText
|
||||||
|
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||||
|
import org.thoughtcrime.securesms.util.VibrateUtil
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.AnyMappingModel
|
||||||
|
import org.thoughtcrime.securesms.util.doOnEachLayout
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for inline search results.
|
||||||
|
*/
|
||||||
|
class InlineQueryResultsController(
|
||||||
|
private val context: Context,
|
||||||
|
private val viewModel: InlineQueryViewModel,
|
||||||
|
private val anchor: View,
|
||||||
|
private val container: ViewGroup,
|
||||||
|
editText: ComposeText,
|
||||||
|
lifecycleOwner: LifecycleOwner
|
||||||
|
) : InlineQueryResultsPopup.Callback {
|
||||||
|
|
||||||
|
private val lifecycleDisposable: LifecycleDisposable = LifecycleDisposable()
|
||||||
|
private var popup: InlineQueryResultsPopup? = null
|
||||||
|
private var previousResults: List<AnyMappingModel>? = null
|
||||||
|
private var canShow: Boolean = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
lifecycleDisposable.bindTo(lifecycleOwner)
|
||||||
|
|
||||||
|
lifecycleDisposable += viewModel.results
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeBy { updateList(it) }
|
||||||
|
|
||||||
|
lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||||
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
editText.addOnFocusChangeListener { _, hasFocus ->
|
||||||
|
canShow = hasFocus
|
||||||
|
updateList(previousResults ?: emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
anchor.doOnEachLayout { popup?.updateWithAnchor() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSelection(model: AnyMappingModel) {
|
||||||
|
viewModel.onSelection(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDismiss() {
|
||||||
|
popup = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onOrientationChange(isLandscape: Boolean) {
|
||||||
|
if (isLandscape) {
|
||||||
|
dismiss()
|
||||||
|
} else {
|
||||||
|
updateList(previousResults ?: emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateList(results: List<AnyMappingModel>) {
|
||||||
|
previousResults = results
|
||||||
|
if (results.isEmpty() || !canShow) {
|
||||||
|
dismiss()
|
||||||
|
} else if (popup != null) {
|
||||||
|
popup?.setResults(results)
|
||||||
|
} else {
|
||||||
|
popup = InlineQueryResultsPopup(
|
||||||
|
anchor = anchor,
|
||||||
|
container = container,
|
||||||
|
results = results,
|
||||||
|
baseOffsetX = DimensionUnit.DP.toPixels(16f).toInt(),
|
||||||
|
callback = this
|
||||||
|
).show()
|
||||||
|
VibrateUtil.vibrateTick(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dismiss() {
|
||||||
|
popup?.dismiss()
|
||||||
|
popup = null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
package org.thoughtcrime.securesms.conversation.ui.inlinequery
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.PopupWindow
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
|
import org.thoughtcrime.securesms.util.ViewUtil
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.AnyMappingModel
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||||
|
|
||||||
|
class InlineQueryResultsPopup(
|
||||||
|
val anchor: View,
|
||||||
|
val container: ViewGroup,
|
||||||
|
results: List<AnyMappingModel>,
|
||||||
|
val baseOffsetX: Int = 0,
|
||||||
|
val baseOffsetY: Int = 0,
|
||||||
|
var callback: Callback?
|
||||||
|
) : PopupWindow(
|
||||||
|
LayoutInflater.from(anchor.context).inflate(R.layout.inline_query_results_popup, null),
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
false
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val context: Context = anchor.context
|
||||||
|
|
||||||
|
private val list: RecyclerView = contentView.findViewById(R.id.inline_query_results_list)
|
||||||
|
private val adapter: MappingAdapter
|
||||||
|
|
||||||
|
init {
|
||||||
|
setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.signal_context_menu_background))
|
||||||
|
inputMethodMode = INPUT_METHOD_NOT_NEEDED
|
||||||
|
|
||||||
|
setOnDismissListener {
|
||||||
|
callback?.onDismiss()
|
||||||
|
callback = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
|
elevation = 20f
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter = InlineQueryAdapter { m -> callback?.onSelection(m) }
|
||||||
|
list.adapter = adapter
|
||||||
|
list.itemAnimator = null
|
||||||
|
|
||||||
|
setResults(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setResults(results: List<AnyMappingModel>) {
|
||||||
|
adapter.submitList(results) { list.scrollToPosition(0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(): InlineQueryResultsPopup {
|
||||||
|
if (anchor.width == 0 || anchor.height == 0) {
|
||||||
|
anchor.post(this::show)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
val (offsetX, offsetY) = calculateOffsets()
|
||||||
|
|
||||||
|
showAsDropDown(anchor, offsetX, offsetY)
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateWithAnchor() {
|
||||||
|
val (offsetX, offsetY) = calculateOffsets()
|
||||||
|
update(anchor, offsetX, offsetY, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateOffsets(): Pair<Int, Int> {
|
||||||
|
contentView.measure(
|
||||||
|
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
||||||
|
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
||||||
|
)
|
||||||
|
|
||||||
|
val anchorRect = Rect(anchor.left, anchor.top, anchor.right, anchor.bottom).also {
|
||||||
|
if (anchor.parent != container) {
|
||||||
|
container.offsetDescendantRectToMyCoords(anchor, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val menuBottomBound = anchorRect.bottom + contentView.measuredHeight + baseOffsetY
|
||||||
|
val menuTopBound = anchorRect.top - contentView.measuredHeight - baseOffsetY
|
||||||
|
|
||||||
|
val screenBottomBound = container.height
|
||||||
|
val screenTopBound = container.y
|
||||||
|
|
||||||
|
val offsetY: Int = when {
|
||||||
|
menuTopBound > screenTopBound -> -(anchorRect.height() + contentView.measuredHeight + baseOffsetY)
|
||||||
|
menuBottomBound < screenBottomBound -> baseOffsetY
|
||||||
|
menuTopBound > screenTopBound -> -(anchorRect.height() + contentView.measuredHeight + baseOffsetY)
|
||||||
|
else -> -((anchorRect.height() / 2) + (contentView.measuredHeight / 2) + baseOffsetY)
|
||||||
|
}
|
||||||
|
|
||||||
|
val offsetX: Int = if (ViewUtil.isLtr(context)) {
|
||||||
|
baseOffsetX
|
||||||
|
} else {
|
||||||
|
-(baseOffsetX + contentView.measuredWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
return offsetX to offsetY
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun onSelection(model: AnyMappingModel)
|
||||||
|
fun onDismiss()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package org.thoughtcrime.securesms.conversation.ui.inlinequery
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||||
|
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchRepository
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.util.adapter.mapping.AnyMappingModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity (at least) scope view model for managing inline queries. The view model needs to be larger scope so it can
|
||||||
|
* be shared between the fragment requesting the search and the instace of [InlineQueryResultsFragment] used for displaying
|
||||||
|
* the results.
|
||||||
|
*/
|
||||||
|
class InlineQueryViewModel(private val emojiSearchRepository: EmojiSearchRepository = EmojiSearchRepository(ApplicationDependencies.getApplication())) : ViewModel() {
|
||||||
|
|
||||||
|
private val querySubject: PublishSubject<InlineQuery> = PublishSubject.create()
|
||||||
|
private val selectionSubject: PublishSubject<InlineQueryReplacement> = PublishSubject.create()
|
||||||
|
|
||||||
|
val results: Observable<List<AnyMappingModel>>
|
||||||
|
val selection: Observable<InlineQueryReplacement> = selectionSubject
|
||||||
|
|
||||||
|
init {
|
||||||
|
results = querySubject.switchMap { query ->
|
||||||
|
when (query) {
|
||||||
|
is InlineQuery.Emoji -> queryEmoji(query)
|
||||||
|
is InlineQuery.Mention -> Observable.just(emptyList())
|
||||||
|
InlineQuery.NoQuery -> Observable.just(emptyList())
|
||||||
|
}
|
||||||
|
}.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onQueryChange(inlineQuery: InlineQuery) {
|
||||||
|
querySubject.onNext(inlineQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun queryEmoji(query: InlineQuery.Emoji): Observable<List<AnyMappingModel>> {
|
||||||
|
return emojiSearchRepository
|
||||||
|
.submitQuery(query.query)
|
||||||
|
.map { r -> toMappingModels(r, query.keywordSearch) }
|
||||||
|
.toObservable()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSelection(model: AnyMappingModel) {
|
||||||
|
when (model) {
|
||||||
|
is InlineQueryEmojiResult.Model -> {
|
||||||
|
selectionSubject.onNext(InlineQueryReplacement.Emoji(model.preferredEmoji, model.keywordSearch))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun toMappingModels(emojiWithLabels: List<String>, keywordSearch: Boolean): List<AnyMappingModel> {
|
||||||
|
val emojiValues = SignalStore.emojiValues()
|
||||||
|
return emojiWithLabels
|
||||||
|
.distinct()
|
||||||
|
.map { emoji ->
|
||||||
|
InlineQueryEmojiResult.Model(
|
||||||
|
canonicalEmoji = emoji,
|
||||||
|
preferredEmoji = emojiValues.getPreferredVariation(emoji),
|
||||||
|
keywordSearch = keywordSearch
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,8 +28,6 @@ public class MentionsPickerFragment extends LoggingFragment {
|
||||||
|
|
||||||
private MentionsPickerAdapter adapter;
|
private MentionsPickerAdapter adapter;
|
||||||
private RecyclerView list;
|
private RecyclerView list;
|
||||||
private View topDivider;
|
|
||||||
private View bottomDivider;
|
|
||||||
private BottomSheetBehavior<View> behavior;
|
private BottomSheetBehavior<View> behavior;
|
||||||
private MentionsPickerViewModel viewModel;
|
private MentionsPickerViewModel viewModel;
|
||||||
private final Runnable lockSheetAfterListUpdate = () -> behavior.setHideable(false);
|
private final Runnable lockSheetAfterListUpdate = () -> behavior.setHideable(false);
|
||||||
|
@ -40,8 +38,6 @@ public class MentionsPickerFragment extends LoggingFragment {
|
||||||
View view = inflater.inflate(R.layout.mentions_picker_fragment, container, false);
|
View view = inflater.inflate(R.layout.mentions_picker_fragment, container, false);
|
||||||
|
|
||||||
list = view.findViewById(R.id.mentions_picker_list);
|
list = view.findViewById(R.id.mentions_picker_list);
|
||||||
topDivider = view.findViewById(R.id.mentions_picker_top_divider);
|
|
||||||
bottomDivider = view.findViewById(R.id.mentions_picker_bottom_divider);
|
|
||||||
behavior = BottomSheetBehavior.from(view.findViewById(R.id.mentions_picker_bottom_sheet));
|
behavior = BottomSheetBehavior.from(view.findViewById(R.id.mentions_picker_bottom_sheet));
|
||||||
|
|
||||||
initializeBehavior();
|
initializeBehavior();
|
||||||
|
@ -74,15 +70,12 @@ public class MentionsPickerFragment extends LoggingFragment {
|
||||||
public void onStateChanged(@NonNull View bottomSheet, int newState) {
|
public void onStateChanged(@NonNull View bottomSheet, int newState) {
|
||||||
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
|
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
|
||||||
adapter.submitList(Collections.emptyList());
|
adapter.submitList(Collections.emptyList());
|
||||||
showDividers(false);
|
|
||||||
} else {
|
} else {
|
||||||
showDividers(true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
|
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
|
||||||
showDividers(Float.isNaN(slideOffset) || slideOffset > -0.8f);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -116,16 +109,10 @@ public class MentionsPickerFragment extends LoggingFragment {
|
||||||
list.scrollToPosition(0);
|
list.scrollToPosition(0);
|
||||||
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
handler.post(lockSheetAfterListUpdate);
|
handler.post(lockSheetAfterListUpdate);
|
||||||
showDividers(true);
|
|
||||||
} else {
|
} else {
|
||||||
handler.removeCallbacks(lockSheetAfterListUpdate);
|
handler.removeCallbacks(lockSheetAfterListUpdate);
|
||||||
behavior.setHideable(true);
|
behavior.setHideable(true);
|
||||||
behavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
behavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showDividers(boolean showDividers) {
|
|
||||||
topDivider.setVisibility(showDividers ? View.VISIBLE : View.GONE);
|
|
||||||
bottomDivider.setVisibility(showDividers ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,10 @@ import android.text.TextUtils;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.model.EmojiSearchData;
|
|
||||||
import org.signal.core.util.CursorUtil;
|
import org.signal.core.util.CursorUtil;
|
||||||
import org.thoughtcrime.securesms.util.FtsUtil;
|
|
||||||
import org.signal.core.util.SqlUtil;
|
import org.signal.core.util.SqlUtil;
|
||||||
|
import org.thoughtcrime.securesms.database.model.EmojiSearchData;
|
||||||
|
import org.thoughtcrime.securesms.util.FtsUtil;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -48,7 +48,7 @@ public class EmojiSearchDatabase extends Database {
|
||||||
String selection = LABEL + " MATCH (?)";
|
String selection = LABEL + " MATCH (?)";
|
||||||
String[] args = SqlUtil.buildArgs(matchString);
|
String[] args = SqlUtil.buildArgs(matchString);
|
||||||
|
|
||||||
try (Cursor cursor = db.query(true, TABLE_NAME, projection, selection, args, null, null,"rank", String.valueOf(limit))) {
|
try (Cursor cursor = db.query(true, TABLE_NAME, projection, selection, args, null, null, "rank", String.valueOf(limit))) {
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
results.add(CursorUtil.requireString(cursor, EMOJI));
|
results.add(CursorUtil.requireString(cursor, EMOJI));
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.keyboard.emoji.search
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import io.reactivex.rxjava3.core.Single
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import org.signal.core.util.concurrent.SignalExecutors
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
import org.thoughtcrime.securesms.components.emoji.Emoji
|
import org.thoughtcrime.securesms.components.emoji.Emoji
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel
|
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel
|
||||||
|
@ -13,12 +15,23 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
|
|
||||||
private const val MINIMUM_QUERY_THRESHOLD = 1
|
private const val MINIMUM_QUERY_THRESHOLD = 1
|
||||||
|
private const val MINIMUM_INLINE_QUERY_THRESHOLD = 2
|
||||||
private const val EMOJI_SEARCH_LIMIT = 20
|
private const val EMOJI_SEARCH_LIMIT = 20
|
||||||
|
|
||||||
class EmojiSearchRepository(private val context: Context) {
|
class EmojiSearchRepository(private val context: Context) {
|
||||||
|
|
||||||
private val emojiSearchDatabase: EmojiSearchDatabase = SignalDatabase.emojiSearch
|
private val emojiSearchDatabase: EmojiSearchDatabase = SignalDatabase.emojiSearch
|
||||||
|
|
||||||
|
fun submitQuery(query: String, limit: Int = EMOJI_SEARCH_LIMIT): Single<List<String>> {
|
||||||
|
if (query.length < MINIMUM_INLINE_QUERY_THRESHOLD) {
|
||||||
|
return Single.just(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
return Single.fromCallable<List<String>> {
|
||||||
|
emojiSearchDatabase.query(query, limit)
|
||||||
|
}.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
fun submitQuery(query: String, includeRecents: Boolean, limit: Int = EMOJI_SEARCH_LIMIT, consumer: Consumer<EmojiPageModel>) {
|
fun submitQuery(query: String, includeRecents: Boolean, limit: Int = EMOJI_SEARCH_LIMIT, consumer: Consumer<EmojiPageModel>) {
|
||||||
if (query.length < MINIMUM_QUERY_THRESHOLD && includeRecents) {
|
if (query.length < MINIMUM_QUERY_THRESHOLD && includeRecents) {
|
||||||
consumer.accept(RecentEmojiPageModel(context, TextSecurePreferences.RECENT_STORAGE_KEY))
|
consumer.accept(RecentEmojiPageModel(context, TextSecurePreferences.RECENT_STORAGE_KEY))
|
||||||
|
|
|
@ -11,6 +11,7 @@ import androidx.fragment.app.FragmentManager
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||||
import org.signal.core.util.EditTextUtil
|
import org.signal.core.util.EditTextUtil
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.ComposeText
|
import org.thoughtcrime.securesms.components.ComposeText
|
||||||
|
@ -19,6 +20,11 @@ import org.thoughtcrime.securesms.components.KeyboardEntryDialogFragment
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle
|
import org.thoughtcrime.securesms.components.emoji.EmojiToggle
|
||||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard
|
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard
|
||||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
|
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQuery
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQuery.NoQuery
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryChangedListener
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryResultsController
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryViewModel
|
||||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerFragment
|
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerFragment
|
||||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel
|
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel
|
||||||
import org.thoughtcrime.securesms.keyboard.KeyboardPage
|
import org.thoughtcrime.securesms.keyboard.KeyboardPage
|
||||||
|
@ -48,11 +54,16 @@ class AddMessageDialogFragment : KeyboardEntryDialogFragment(R.layout.v2_media_a
|
||||||
factoryProducer = { MentionsPickerViewModel.Factory() }
|
factoryProducer = { MentionsPickerViewModel.Factory() }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val inlineQueryViewModel: InlineQueryViewModel by viewModels(
|
||||||
|
ownerProducer = { requireActivity() }
|
||||||
|
)
|
||||||
|
|
||||||
private lateinit var input: ComposeText
|
private lateinit var input: ComposeText
|
||||||
private lateinit var emojiDrawerToggle: EmojiToggle
|
private lateinit var emojiDrawerToggle: EmojiToggle
|
||||||
private lateinit var emojiDrawerStub: Stub<MediaKeyboard>
|
private lateinit var emojiDrawerStub: Stub<MediaKeyboard>
|
||||||
private lateinit var hud: InputAwareLayout
|
private lateinit var hud: InputAwareLayout
|
||||||
private lateinit var mentionsContainer: ViewGroup
|
private lateinit var mentionsContainer: ViewGroup
|
||||||
|
private lateinit var inlineQueryResultsController: InlineQueryResultsController
|
||||||
|
|
||||||
private var requestedEmojiDrawer: Boolean = false
|
private var requestedEmojiDrawer: Boolean = false
|
||||||
|
|
||||||
|
@ -136,7 +147,7 @@ class AddMessageDialogFragment : KeyboardEntryDialogFragment(R.layout.v2_media_a
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
disposables.dispose()
|
disposables.dispose()
|
||||||
|
|
||||||
input.setMentionQueryChangedListener(null)
|
input.setInlineQueryChangedListener(null)
|
||||||
input.setMentionValidator(null)
|
input.setMentionValidator(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,15 +156,43 @@ class AddMessageDialogFragment : KeyboardEntryDialogFragment(R.layout.v2_media_a
|
||||||
|
|
||||||
mentionsContainer = requireView().findViewById(R.id.mentions_picker_container)
|
mentionsContainer = requireView().findViewById(R.id.mentions_picker_container)
|
||||||
|
|
||||||
|
inlineQueryResultsController = InlineQueryResultsController(
|
||||||
|
requireContext(),
|
||||||
|
inlineQueryViewModel,
|
||||||
|
requireView().findViewById(R.id.background_holder),
|
||||||
|
(requireView() as ViewGroup),
|
||||||
|
input,
|
||||||
|
viewLifecycleOwner
|
||||||
|
)
|
||||||
|
|
||||||
Recipient.live(recipientId).observe(viewLifecycleOwner) { recipient ->
|
Recipient.live(recipientId).observe(viewLifecycleOwner) { recipient ->
|
||||||
mentionsViewModel.onRecipientChange(recipient)
|
mentionsViewModel.onRecipientChange(recipient)
|
||||||
|
|
||||||
input.setMentionQueryChangedListener { query ->
|
input.setInlineQueryChangedListener(object : InlineQueryChangedListener {
|
||||||
if (recipient.isPushV2Group) {
|
override fun onQueryChanged(inlineQuery: InlineQuery) {
|
||||||
ensureMentionsContainerFilled()
|
when (inlineQuery) {
|
||||||
mentionsViewModel.onQueryChange(query)
|
is InlineQuery.Mention -> {
|
||||||
|
if (recipient.isPushV2Group && recipient.isActiveGroup) {
|
||||||
|
ensureMentionsContainerFilled()
|
||||||
|
mentionsViewModel.onQueryChange(inlineQuery.query)
|
||||||
|
}
|
||||||
|
inlineQueryViewModel.onQueryChange(inlineQuery)
|
||||||
|
}
|
||||||
|
is InlineQuery.Emoji -> {
|
||||||
|
inlineQueryViewModel.onQueryChange(inlineQuery)
|
||||||
|
mentionsViewModel.onQueryChange(null)
|
||||||
|
}
|
||||||
|
is NoQuery -> {
|
||||||
|
mentionsViewModel.onQueryChange(null)
|
||||||
|
inlineQueryViewModel.onQueryChange(inlineQuery)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
override fun clearQuery() {
|
||||||
|
onQueryChanged(NoQuery)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
input.setMentionValidator { annotations ->
|
input.setMentionValidator { annotations ->
|
||||||
if (!recipient.isPushV2Group) {
|
if (!recipient.isPushV2Group) {
|
||||||
|
@ -174,6 +213,11 @@ class AddMessageDialogFragment : KeyboardEntryDialogFragment(R.layout.v2_media_a
|
||||||
mentionsViewModel.selectedRecipient.observe(viewLifecycleOwner) { recipient ->
|
mentionsViewModel.selectedRecipient.observe(viewLifecycleOwner) { recipient ->
|
||||||
input.replaceTextWithMention(recipient.getDisplayName(requireContext()), recipient.id)
|
input.replaceTextWithMention(recipient.getDisplayName(requireContext()), recipient.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disposables += inlineQueryViewModel
|
||||||
|
.selection
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { r -> input.replaceText(r) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureMentionsContainerFilled() {
|
private fun ensureMentionsContainerFilled() {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.os.Bundle
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
@ -15,6 +16,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehaviorHack
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
import org.signal.core.util.concurrent.SignalExecutors
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
|
@ -27,6 +29,10 @@ import org.thoughtcrime.securesms.components.settings.configure
|
||||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||||
import org.thoughtcrime.securesms.conversation.MarkReadHelper
|
import org.thoughtcrime.securesms.conversation.MarkReadHelper
|
||||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQuery
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryChangedListener
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryResultsController
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryViewModel
|
||||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerFragment
|
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerFragment
|
||||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel
|
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel
|
||||||
import org.thoughtcrime.securesms.database.model.Mention
|
import org.thoughtcrime.securesms.database.model.Mention
|
||||||
|
@ -101,6 +107,10 @@ class StoryGroupReplyFragment :
|
||||||
ownerProducer = { requireActivity() }
|
ownerProducer = { requireActivity() }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val inlineQueryViewModel: InlineQueryViewModel by viewModels(
|
||||||
|
ownerProducer = { requireActivity() }
|
||||||
|
)
|
||||||
|
|
||||||
private val keyboardPagerViewModel: KeyboardPagerViewModel by viewModels(
|
private val keyboardPagerViewModel: KeyboardPagerViewModel by viewModels(
|
||||||
ownerProducer = { requireActivity() }
|
ownerProducer = { requireActivity() }
|
||||||
)
|
)
|
||||||
|
@ -145,6 +155,8 @@ class StoryGroupReplyFragment :
|
||||||
private var resendMentions: List<Mention> = emptyList()
|
private var resendMentions: List<Mention> = emptyList()
|
||||||
private var resendReaction: String? = null
|
private var resendReaction: String? = null
|
||||||
|
|
||||||
|
private lateinit var inlineQueryResultsController: InlineQueryResultsController
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
SignalExecutors.BOUNDED.execute {
|
SignalExecutors.BOUNDED.execute {
|
||||||
RetrieveProfileJob.enqueue(groupRecipientId)
|
RetrieveProfileJob.enqueue(groupRecipientId)
|
||||||
|
@ -227,7 +239,7 @@ class StoryGroupReplyFragment :
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
|
|
||||||
composer.input.setMentionQueryChangedListener(null)
|
composer.input.setInlineQueryChangedListener(null)
|
||||||
composer.input.setMentionValidator(null)
|
composer.input.setMentionValidator(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -422,15 +434,43 @@ class StoryGroupReplyFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeMentions() {
|
private fun initializeMentions() {
|
||||||
|
inlineQueryResultsController = InlineQueryResultsController(
|
||||||
|
requireContext(),
|
||||||
|
inlineQueryViewModel,
|
||||||
|
composer,
|
||||||
|
(requireView() as ViewGroup),
|
||||||
|
composer.input,
|
||||||
|
viewLifecycleOwner
|
||||||
|
)
|
||||||
|
|
||||||
Recipient.live(groupRecipientId).observe(viewLifecycleOwner) { recipient ->
|
Recipient.live(groupRecipientId).observe(viewLifecycleOwner) { recipient ->
|
||||||
mentionsViewModel.onRecipientChange(recipient)
|
mentionsViewModel.onRecipientChange(recipient)
|
||||||
|
|
||||||
composer.input.setMentionQueryChangedListener { query ->
|
composer.input.setInlineQueryChangedListener(object : InlineQueryChangedListener {
|
||||||
if (recipient.isPushV2Group) {
|
override fun onQueryChanged(inlineQuery: InlineQuery) {
|
||||||
ensureMentionsContainerFilled()
|
when (inlineQuery) {
|
||||||
mentionsViewModel.onQueryChange(query)
|
is InlineQuery.Mention -> {
|
||||||
|
if (recipient.isPushV2Group) {
|
||||||
|
ensureMentionsContainerFilled()
|
||||||
|
mentionsViewModel.onQueryChange(inlineQuery.query)
|
||||||
|
}
|
||||||
|
inlineQueryViewModel.onQueryChange(inlineQuery)
|
||||||
|
}
|
||||||
|
is InlineQuery.Emoji -> {
|
||||||
|
inlineQueryViewModel.onQueryChange(inlineQuery)
|
||||||
|
mentionsViewModel.onQueryChange(null)
|
||||||
|
}
|
||||||
|
is InlineQuery.NoQuery -> {
|
||||||
|
mentionsViewModel.onQueryChange(null)
|
||||||
|
inlineQueryViewModel.onQueryChange(inlineQuery)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
override fun clearQuery() {
|
||||||
|
onQueryChanged(InlineQuery.NoQuery)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
composer.input.setMentionValidator { annotations ->
|
composer.input.setMentionValidator { annotations ->
|
||||||
if (!recipient.isPushV2Group) {
|
if (!recipient.isPushV2Group) {
|
||||||
|
@ -452,6 +492,11 @@ class StoryGroupReplyFragment :
|
||||||
composer.input.replaceTextWithMention(recipient.getDisplayName(requireContext()), recipient.id)
|
composer.input.replaceTextWithMention(recipient.getDisplayName(requireContext()), recipient.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lifecycleDisposable += inlineQueryViewModel
|
||||||
|
.selection
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { r -> composer.input.replaceText(r) }
|
||||||
|
|
||||||
mentionsViewModel.isShowing.observe(viewLifecycleOwner) { updateNestedScrolling() }
|
mentionsViewModel.isShowing.observe(viewLifecycleOwner) { updateNestedScrolling() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,3 +22,9 @@ fun ConstraintLayout.changeConstraints(change: ConstraintSet.() -> Unit) {
|
||||||
set.change()
|
set.change()
|
||||||
set.applyTo(this)
|
set.applyTo(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun View.doOnEachLayout(crossinline action: (view: View) -> Unit): View.OnLayoutChangeListener {
|
||||||
|
val listener = View.OnLayoutChangeListener { view, _, _, _, _, _, _, _, _ -> action(view) }
|
||||||
|
addOnLayoutChangeListener(listener)
|
||||||
|
return listener
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
package org.thoughtcrime.securesms.util.adapter.mapping
|
||||||
|
|
||||||
|
/** Syntactic sugar for wildcard generic */
|
||||||
|
typealias AnyMappingModel = MappingModel<*>
|
16
app/src/main/res/layout/inline_query_emoji_result.xml
Normal file
16
app/src/main/res/layout/inline_query_emoji_result.xml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
android:paddingHorizontal="4dp"
|
||||||
|
android:paddingVertical="8dp">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||||
|
android:id="@+id/inline_query_emoji_image"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
tools:src="@drawable/ic_emoji_smiley_24" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
17
app/src/main/res/layout/inline_query_results_popup.xml
Normal file
17
app/src/main/res/layout/inline_query_results_popup.xml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/inline_query_results_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
|
@ -10,32 +10,16 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
app:behavior_peekHeight="236dp"
|
app:behavior_peekHeight="216dp"
|
||||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/mentions_picker_top_divider"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="2dp"
|
|
||||||
android:layout_gravity="top"
|
|
||||||
android:layout_marginTop="-2dp"
|
|
||||||
android:background="@drawable/compose_divider_background"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/mentions_picker_list"
|
android:id="@+id/mentions_picker_list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@color/signal_background_dialog" />
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
android:background="@color/signal_colorSurface1" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/mentions_picker_bottom_divider"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="2dp"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:background="@drawable/compose_divider_background"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
Loading…
Add table
Reference in a new issue