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.InputContentInfoCompat;
|
||||
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
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.MentionValidatorWatcher;
|
||||
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.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.thoughtcrime.securesms.database.MentionUtil.MENTION_STARTER;
|
||||
|
||||
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 SpannableString subHint;
|
||||
private MentionRendererDelegate mentionRendererDelegate;
|
||||
|
@ -54,7 +61,14 @@ public class ComposeText extends EmojiEditText {
|
|||
|
||||
@Nullable private InputPanel.MediaListener mediaListener;
|
||||
@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) {
|
||||
super(context);
|
||||
|
@ -111,7 +125,7 @@ public class ComposeText extends EmojiEditText {
|
|||
if (selectionStart == selectionEnd) {
|
||||
doAfterCursorChange(getText());
|
||||
} else {
|
||||
updateQuery(null);
|
||||
clearInlineQuery();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -189,8 +203,8 @@ public class ComposeText extends EmojiEditText {
|
|||
this.cursorPositionChangedListener = listener;
|
||||
}
|
||||
|
||||
public void setMentionQueryChangedListener(@Nullable MentionQueryChangedListener listener) {
|
||||
this.mentionQueryChangedListener = listener;
|
||||
public void setInlineQueryChangedListener(@Nullable InlineQueryChangedListener listener) {
|
||||
this.inlineQueryChangedListener = listener;
|
||||
}
|
||||
|
||||
public void setMentionValidator(@Nullable MentionValidatorWatcher.MentionValidator mentionValidator) {
|
||||
|
@ -226,15 +240,23 @@ public class ComposeText extends EmojiEditText {
|
|||
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
|
||||
InputConnection inputConnection = super.onCreateInputConnection(editorInfo);
|
||||
|
||||
if(SignalStore.settings().isEnterKeySends()) {
|
||||
if (SignalStore.settings().isEnterKeySends()) {
|
||||
editorInfo.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < 21) return inputConnection;
|
||||
if (mediaListener == null) return inputConnection;
|
||||
if (inputConnection == null) return null;
|
||||
if (Build.VERSION.SDK_INT < 21) {
|
||||
return inputConnection;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
@ -300,35 +322,53 @@ public class ComposeText extends EmojiEditText {
|
|||
}
|
||||
|
||||
private void doAfterCursorChange(@NonNull Editable text) {
|
||||
if (enoughToFilter(text)) {
|
||||
performFiltering(text);
|
||||
removeCallbacks(keywordSearchRunnable);
|
||||
if (enoughToFilter(text, false)) {
|
||||
performFiltering(text, false);
|
||||
} else {
|
||||
updateQuery(null);
|
||||
postDelayed(keywordSearchRunnable, EMOJI_KEYWORD_DELAY);
|
||||
clearInlineQuery();
|
||||
}
|
||||
}
|
||||
|
||||
private void performFiltering(@NonNull Editable text) {
|
||||
int end = getSelectionEnd();
|
||||
int start = findQueryStart(text, end);
|
||||
CharSequence query = text.subSequence(start, end);
|
||||
updateQuery(query.toString());
|
||||
}
|
||||
private void performFiltering(@NonNull Editable text, boolean keywordEmojiSearch) {
|
||||
int end = getSelectionEnd();
|
||||
QueryStart queryStart = findQueryStart(text, end, keywordEmojiSearch);
|
||||
int start = queryStart.index;
|
||||
String query = text.subSequence(start, end).toString();
|
||||
|
||||
private void updateQuery(@Nullable String query) {
|
||||
if (mentionQueryChangedListener != null) {
|
||||
mentionQueryChangedListener.onQueryChanged(query);
|
||||
if (inlineQueryChangedListener != null) {
|
||||
if (queryStart.isMentionQuery) {
|
||||
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();
|
||||
if (end < 0) {
|
||||
return false;
|
||||
}
|
||||
return findQueryStart(text, end) != -1;
|
||||
return findQueryStart(text, end, keywordEmojiSearch).index != -1;
|
||||
}
|
||||
|
||||
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();
|
||||
if (text == null) {
|
||||
return;
|
||||
|
@ -336,10 +376,11 @@ public class ComposeText extends EmojiEditText {
|
|||
|
||||
clearComposingText();
|
||||
|
||||
int end = getSelectionEnd();
|
||||
int start = findQueryStart(text, end) - 1;
|
||||
int end = getSelectionEnd();
|
||||
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) {
|
||||
|
@ -357,17 +398,37 @@ public class ComposeText extends EmojiEditText {
|
|||
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) {
|
||||
return -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--;
|
||||
}
|
||||
|
||||
if (delimiterSearchIndex >= 0 && text.charAt(delimiterSearchIndex) == MENTION_STARTER) {
|
||||
if (delimiterSearchIndex >= 0 && text.charAt(delimiterSearchIndex) == starter) {
|
||||
return delimiterSearchIndex + 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 {
|
||||
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.appcompat.widget.AppCompatEditText;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
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) {
|
||||
this(context, null);
|
||||
|
@ -38,6 +41,12 @@ public class EmojiEditText extends AppCompatEditText {
|
|||
if (!isInEditMode() && (forceCustom || !SignalStore.settings().isPreferSystemEmoji())) {
|
||||
setFilters(appendEmojiFilter(this.getFilters(), jumboEmoji));
|
||||
}
|
||||
|
||||
super.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
for (OnFocusChangeListener listener : onFocusChangeListeners) {
|
||||
listener.onFocusChange(v, hasFocus);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void insertEmoji(String emoji) {
|
||||
|
@ -54,6 +63,17 @@ public class EmojiEditText extends AppCompatEditText {
|
|||
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) {
|
||||
InputFilter[] result;
|
||||
|
||||
|
|
|
@ -133,11 +133,11 @@ class SignalContextMenu private constructor(
|
|||
val container: ViewGroup
|
||||
) {
|
||||
|
||||
var onDismiss: Runnable? = null
|
||||
var offsetX = 0
|
||||
var offsetY = 0
|
||||
var horizontalPosition = HorizontalPosition.START
|
||||
var verticalPosition = VerticalPosition.BELOW
|
||||
private var onDismiss: Runnable? = null
|
||||
private var offsetX = 0
|
||||
private var offsetY = 0
|
||||
private var horizontalPosition = HorizontalPosition.START
|
||||
private var verticalPosition = VerticalPosition.BELOW
|
||||
|
||||
fun onDismiss(onDismiss: Runnable): Builder {
|
||||
this.onDismiss = onDismiss
|
||||
|
|
|
@ -103,6 +103,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
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.location.SignalPlace;
|
||||
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.ExpiredBuildReminder;
|
||||
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.DraftViewModel;
|
||||
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.crypto.ReentrantSessionLock;
|
||||
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.AtomicInteger;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
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;
|
||||
|
||||
/**
|
||||
|
@ -443,12 +454,14 @@ public class ConversationParentFragment extends Fragment
|
|||
private InviteReminderModel inviteReminderModel;
|
||||
private ConversationGroupViewModel groupViewModel;
|
||||
private MentionsPickerViewModel mentionsViewModel;
|
||||
private InlineQueryViewModel inlineQueryViewModel;
|
||||
private GroupCallViewModel groupCallViewModel;
|
||||
private VoiceRecorderWakeLock voiceRecorderWakeLock;
|
||||
private DraftViewModel draftViewModel;
|
||||
private VoiceNoteMediaController voiceNoteMediaController;
|
||||
private VoiceNotePlayerView voiceNotePlayerView;
|
||||
private Material3OnScrollHelper material3OnScrollHelper;
|
||||
private InlineQueryResultsController inlineQueryResultsController;
|
||||
|
||||
private LiveRecipient recipient;
|
||||
private long threadId;
|
||||
|
@ -644,6 +657,10 @@ public class ConversationParentFragment extends Fragment
|
|||
if (reactionDelegate.isShowing()) {
|
||||
reactionDelegate.hide();
|
||||
}
|
||||
|
||||
if (inlineQueryResultsController != null) {
|
||||
inlineQueryResultsController.onOrientationChange(newConfig.orientation == ORIENTATION_LANDSCAPE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -2330,7 +2347,17 @@ public class ConversationParentFragment extends Fragment
|
|||
}
|
||||
|
||||
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 -> {
|
||||
if (r.isPushV2Group() && !mentionsSuggestions.resolved()) {
|
||||
|
@ -2339,12 +2366,29 @@ public class ConversationParentFragment extends Fragment
|
|||
mentionsViewModel.onRecipientChange(r);
|
||||
});
|
||||
|
||||
composeText.setMentionQueryChangedListener(query -> {
|
||||
if (getRecipient().isPushV2Group() && getRecipient().isActiveGroup()) {
|
||||
if (!mentionsSuggestions.resolved()) {
|
||||
mentionsSuggestions.get();
|
||||
composeText.setInlineQueryChangedListener(new InlineQueryChangedListener() {
|
||||
@Override
|
||||
public void onQueryChanged(@NonNull InlineQuery inlineQuery) {
|
||||
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 -> {
|
||||
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() {
|
||||
|
@ -3776,6 +3829,7 @@ public class ConversationParentFragment extends Fragment
|
|||
reactionDelegate.setOnActionSelectedListener(onActionSelectedListener);
|
||||
reactionDelegate.setOnHideListener(onHideListener);
|
||||
reactionDelegate.show(requireActivity(), recipient.get(), conversationMessage, groupViewModel.isNonAdminInAnnouncementGroup(), selectedConversationModel);
|
||||
composeText.clearFocus();
|
||||
if (attachmentKeyboardStub.resolved()) {
|
||||
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 RecyclerView list;
|
||||
private View topDivider;
|
||||
private View bottomDivider;
|
||||
private BottomSheetBehavior<View> behavior;
|
||||
private MentionsPickerViewModel viewModel;
|
||||
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);
|
||||
|
||||
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));
|
||||
|
||||
initializeBehavior();
|
||||
|
@ -74,15 +70,12 @@ public class MentionsPickerFragment extends LoggingFragment {
|
|||
public void onStateChanged(@NonNull View bottomSheet, int newState) {
|
||||
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
adapter.submitList(Collections.emptyList());
|
||||
showDividers(false);
|
||||
} else {
|
||||
showDividers(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
handler.post(lockSheetAfterListUpdate);
|
||||
showDividers(true);
|
||||
} else {
|
||||
handler.removeCallbacks(lockSheetAfterListUpdate);
|
||||
behavior.setHideable(true);
|
||||
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 org.thoughtcrime.securesms.database.model.EmojiSearchData;
|
||||
import org.signal.core.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.FtsUtil;
|
||||
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.List;
|
||||
|
@ -48,7 +48,7 @@ public class EmojiSearchDatabase extends Database {
|
|||
String selection = LABEL + " MATCH (?)";
|
||||
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()) {
|
||||
results.add(CursorUtil.requireString(cursor, EMOJI));
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.keyboard.emoji.search
|
|||
|
||||
import android.content.Context
|
||||
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.thoughtcrime.securesms.components.emoji.Emoji
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel
|
||||
|
@ -13,12 +15,23 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
|
|||
import java.util.function.Consumer
|
||||
|
||||
private const val MINIMUM_QUERY_THRESHOLD = 1
|
||||
private const val MINIMUM_INLINE_QUERY_THRESHOLD = 2
|
||||
private const val EMOJI_SEARCH_LIMIT = 20
|
||||
|
||||
class EmojiSearchRepository(private val context: Context) {
|
||||
|
||||
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>) {
|
||||
if (query.length < MINIMUM_QUERY_THRESHOLD && includeRecents) {
|
||||
consumer.accept(RecentEmojiPageModel(context, TextSecurePreferences.RECENT_STORAGE_KEY))
|
||||
|
|
|
@ -11,6 +11,7 @@ import androidx.fragment.app.FragmentManager
|
|||
import androidx.fragment.app.viewModels
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.signal.core.util.EditTextUtil
|
||||
import org.thoughtcrime.securesms.R
|
||||
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.MediaKeyboard
|
||||
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.MentionsPickerViewModel
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPage
|
||||
|
@ -48,11 +54,16 @@ class AddMessageDialogFragment : KeyboardEntryDialogFragment(R.layout.v2_media_a
|
|||
factoryProducer = { MentionsPickerViewModel.Factory() }
|
||||
)
|
||||
|
||||
private val inlineQueryViewModel: InlineQueryViewModel by viewModels(
|
||||
ownerProducer = { requireActivity() }
|
||||
)
|
||||
|
||||
private lateinit var input: ComposeText
|
||||
private lateinit var emojiDrawerToggle: EmojiToggle
|
||||
private lateinit var emojiDrawerStub: Stub<MediaKeyboard>
|
||||
private lateinit var hud: InputAwareLayout
|
||||
private lateinit var mentionsContainer: ViewGroup
|
||||
private lateinit var inlineQueryResultsController: InlineQueryResultsController
|
||||
|
||||
private var requestedEmojiDrawer: Boolean = false
|
||||
|
||||
|
@ -136,7 +147,7 @@ class AddMessageDialogFragment : KeyboardEntryDialogFragment(R.layout.v2_media_a
|
|||
super.onDestroyView()
|
||||
disposables.dispose()
|
||||
|
||||
input.setMentionQueryChangedListener(null)
|
||||
input.setInlineQueryChangedListener(null)
|
||||
input.setMentionValidator(null)
|
||||
}
|
||||
|
||||
|
@ -145,15 +156,43 @@ class AddMessageDialogFragment : KeyboardEntryDialogFragment(R.layout.v2_media_a
|
|||
|
||||
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 ->
|
||||
mentionsViewModel.onRecipientChange(recipient)
|
||||
|
||||
input.setMentionQueryChangedListener { query ->
|
||||
if (recipient.isPushV2Group) {
|
||||
ensureMentionsContainerFilled()
|
||||
mentionsViewModel.onQueryChange(query)
|
||||
input.setInlineQueryChangedListener(object : InlineQueryChangedListener {
|
||||
override fun onQueryChanged(inlineQuery: InlineQuery) {
|
||||
when (inlineQuery) {
|
||||
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 ->
|
||||
if (!recipient.isPushV2Group) {
|
||||
|
@ -174,6 +213,11 @@ class AddMessageDialogFragment : KeyboardEntryDialogFragment(R.layout.v2_media_a
|
|||
mentionsViewModel.selectedRecipient.observe(viewLifecycleOwner) { recipient ->
|
||||
input.replaceTextWithMention(recipient.getDisplayName(requireContext()), recipient.id)
|
||||
}
|
||||
|
||||
disposables += inlineQueryViewModel
|
||||
.selection
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { r -> input.replaceText(r) }
|
||||
}
|
||||
|
||||
private fun ensureMentionsContainerFilled() {
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.os.Bundle
|
|||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorInt
|
||||
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.dialog.MaterialAlertDialogBuilder
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
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.conversation.MarkReadHelper
|
||||
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.MentionsPickerViewModel
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
|
@ -101,6 +107,10 @@ class StoryGroupReplyFragment :
|
|||
ownerProducer = { requireActivity() }
|
||||
)
|
||||
|
||||
private val inlineQueryViewModel: InlineQueryViewModel by viewModels(
|
||||
ownerProducer = { requireActivity() }
|
||||
)
|
||||
|
||||
private val keyboardPagerViewModel: KeyboardPagerViewModel by viewModels(
|
||||
ownerProducer = { requireActivity() }
|
||||
)
|
||||
|
@ -145,6 +155,8 @@ class StoryGroupReplyFragment :
|
|||
private var resendMentions: List<Mention> = emptyList()
|
||||
private var resendReaction: String? = null
|
||||
|
||||
private lateinit var inlineQueryResultsController: InlineQueryResultsController
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
RetrieveProfileJob.enqueue(groupRecipientId)
|
||||
|
@ -227,7 +239,7 @@ class StoryGroupReplyFragment :
|
|||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
|
||||
composer.input.setMentionQueryChangedListener(null)
|
||||
composer.input.setInlineQueryChangedListener(null)
|
||||
composer.input.setMentionValidator(null)
|
||||
}
|
||||
|
||||
|
@ -422,15 +434,43 @@ class StoryGroupReplyFragment :
|
|||
}
|
||||
|
||||
private fun initializeMentions() {
|
||||
inlineQueryResultsController = InlineQueryResultsController(
|
||||
requireContext(),
|
||||
inlineQueryViewModel,
|
||||
composer,
|
||||
(requireView() as ViewGroup),
|
||||
composer.input,
|
||||
viewLifecycleOwner
|
||||
)
|
||||
|
||||
Recipient.live(groupRecipientId).observe(viewLifecycleOwner) { recipient ->
|
||||
mentionsViewModel.onRecipientChange(recipient)
|
||||
|
||||
composer.input.setMentionQueryChangedListener { query ->
|
||||
if (recipient.isPushV2Group) {
|
||||
ensureMentionsContainerFilled()
|
||||
mentionsViewModel.onQueryChange(query)
|
||||
composer.input.setInlineQueryChangedListener(object : InlineQueryChangedListener {
|
||||
override fun onQueryChanged(inlineQuery: InlineQuery) {
|
||||
when (inlineQuery) {
|
||||
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 ->
|
||||
if (!recipient.isPushV2Group) {
|
||||
|
@ -452,6 +492,11 @@ class StoryGroupReplyFragment :
|
|||
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() }
|
||||
}
|
||||
|
||||
|
|
|
@ -22,3 +22,9 @@ fun ConstraintLayout.changeConstraints(change: ConstraintSet.() -> Unit) {
|
|||
set.change()
|
||||
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_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:behavior_peekHeight="236dp"
|
||||
app:behavior_peekHeight="216dp"
|
||||
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
|
||||
android:id="@+id/mentions_picker_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/signal_background_dialog" />
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
android:background="@color/signal_colorSurface1" />
|
||||
|
||||
</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>
|
||||
|
|
Loading…
Add table
Reference in a new issue