mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 862771641d | |||
| 2128a1718f | |||
| 7e633a2597 | |||
| 2ffe663798 | |||
| 1453bd1314 |
@@ -210,6 +210,9 @@ dependencies {
|
||||
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
|
||||
implementation "io.noties.markwon:inline-parser:$markwon_version"
|
||||
implementation 'com.airbnb.android:lottie:4.2.2' // Lottie animations support.
|
||||
implementation ('com.vdurmont:emoji-java:5.1.1') { // emoji dataset for auto-completion
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
def media3_version = "1.8.0" // 1.9.0 need minSdkVersion 23
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ import org.thoughtcrime.securesms.components.ScaleStableImageView;
|
||||
import org.thoughtcrime.securesms.components.SendButton;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiSearch;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.connect.AccountManager;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
@@ -976,6 +977,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
container.addOnKeyboardShownListener(backgroundView);
|
||||
inputPanel.setListener(this);
|
||||
inputPanel.setMediaListener(this);
|
||||
inputPanel.setEmojiSuggestionListener(emoji -> insertEmojiSuggestion(emoji));
|
||||
|
||||
attachmentTypeSelector = null;
|
||||
attachmentManager = new AttachmentManager(this, this);
|
||||
@@ -1593,13 +1595,84 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
if (composeText.getTextTrimmed().length() == 0 || beforeLength == 0) {
|
||||
composeText.postDelayed(ConversationActivity.this::updateToggleButtonState, 50);
|
||||
}
|
||||
updateEmojiSuggestions(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before,int count) {}
|
||||
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {}
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
if (!hasFocus) inputPanel.hideEmojiSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
private String lastEmojiQuery = null;
|
||||
|
||||
private void updateEmojiSuggestions(CharSequence text) {
|
||||
String query = getEmojiQuery(text, composeText.getSelectionEnd());
|
||||
if (query == null) {
|
||||
lastEmojiQuery = null;
|
||||
inputPanel.hideEmojiSuggestions();
|
||||
return;
|
||||
}
|
||||
if (query.equals(lastEmojiQuery)) return;
|
||||
lastEmojiQuery = query;
|
||||
Util.runOnBackground(() -> {
|
||||
List<String> results = EmojiSearch.search(query);
|
||||
Util.runOnMain(() -> {
|
||||
if (!query.equals(lastEmojiQuery)) return; // stale result
|
||||
if (results.isEmpty()) {
|
||||
inputPanel.hideEmojiSuggestions();
|
||||
} else {
|
||||
inputPanel.showEmojiSuggestions(results);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the emoji search query if the cursor is positioned after a `:word` pattern,
|
||||
* or null if there is no active emoji query.
|
||||
*/
|
||||
private String getEmojiQuery(CharSequence text, int cursorPos) {
|
||||
if (text == null || cursorPos <= 0) return null;
|
||||
int colonPos = -1;
|
||||
for (int i = cursorPos - 1; i >= 0; i--) {
|
||||
char c = text.charAt(i);
|
||||
if (c == ':') {
|
||||
colonPos = i;
|
||||
break;
|
||||
}
|
||||
if (!Character.isLetterOrDigit(c) && c != '_') break;
|
||||
}
|
||||
if (colonPos < 0) return null;
|
||||
String query = text.subSequence(colonPos + 1, cursorPos).toString();
|
||||
if (query.length() < 2) return null;
|
||||
return query;
|
||||
}
|
||||
|
||||
private void insertEmojiSuggestion(String emoji) {
|
||||
android.text.Editable editable = composeText.getText();
|
||||
if (editable == null) return;
|
||||
int cursorPos = composeText.getSelectionEnd();
|
||||
if (cursorPos < 0) return;
|
||||
// Find colon before the query at the cursor position
|
||||
int colonPos = -1;
|
||||
for (int i = cursorPos - 1; i >= 0; i--) {
|
||||
char c = editable.charAt(i);
|
||||
if (c == ':') {
|
||||
colonPos = i;
|
||||
break;
|
||||
}
|
||||
if (!Character.isLetterOrDigit(c) && c != '_') break;
|
||||
}
|
||||
if (colonPos < 0) return;
|
||||
String query = editable.subSequence(colonPos + 1, cursorPos).toString();
|
||||
if (query.length() < 2) return;
|
||||
editable.replace(colonPos, cursorPos, emoji + " ");
|
||||
lastEmojiQuery = null;
|
||||
inputPanel.hideEmojiSuggestions();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,11 +21,14 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiSuggestionAdapter;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
@@ -37,6 +40,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
import org.thoughtcrime.securesms.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@@ -62,6 +66,8 @@ public class InputPanel extends ConstraintLayout
|
||||
private View buttonToggle;
|
||||
private View recordingContainer;
|
||||
private View recordLockCancel;
|
||||
private RecyclerView emojiSuggestions;
|
||||
private EmojiSuggestionAdapter emojiSuggestionAdapter;
|
||||
|
||||
private MicrophoneRecorderView microphoneRecorderView;
|
||||
private SlideToCancel slideToCancel;
|
||||
@@ -103,6 +109,11 @@ public class InputPanel extends ConstraintLayout
|
||||
this.microphoneRecorderView = findViewById(R.id.recorder_view);
|
||||
this.microphoneRecorderView.setListener(this);
|
||||
|
||||
this.emojiSuggestions = findViewById(R.id.emoji_suggestions);
|
||||
this.emojiSuggestionAdapter = new EmojiSuggestionAdapter();
|
||||
this.emojiSuggestions.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
|
||||
this.emojiSuggestions.setAdapter(emojiSuggestionAdapter);
|
||||
|
||||
this.recordLockCancel.setOnClickListener(v -> microphoneRecorderView.cancelAction());
|
||||
|
||||
quoteDismiss.setOnClickListener(v -> clearQuote());
|
||||
@@ -229,6 +240,20 @@ public class InputPanel extends ConstraintLayout
|
||||
mediaKeyboard.setKeyboardListener(this);
|
||||
}
|
||||
|
||||
public void setEmojiSuggestionListener(@NonNull EmojiSuggestionAdapter.OnEmojiClickListener listener) {
|
||||
emojiSuggestionAdapter.setOnEmojiClickListener(listener);
|
||||
}
|
||||
|
||||
public void showEmojiSuggestions(@NonNull List<String> emojis) {
|
||||
emojiSuggestionAdapter.setEmojis(emojis);
|
||||
emojiSuggestions.setVisibility(View.VISIBLE);
|
||||
emojiSuggestions.scrollToPosition(0);
|
||||
}
|
||||
|
||||
public void hideEmojiSuggestions() {
|
||||
emojiSuggestions.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecordPermissionRequired() {
|
||||
if (listener != null) listener.onRecorderPermissionRequired();
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.vdurmont.emoji.Emoji;
|
||||
import com.vdurmont.emoji.EmojiManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides emoji search functionality using the emoji-java library.
|
||||
* Used for emoji auto-completion when user types :query in the compose field.
|
||||
*/
|
||||
public final class EmojiSearch {
|
||||
|
||||
private static final int MAX_RESULTS = 20;
|
||||
|
||||
private EmojiSearch() {}
|
||||
|
||||
/**
|
||||
* Search for emojis whose aliases or tags contain the given query (case-insensitive).
|
||||
* Returns up to MAX_RESULTS emoji Unicode strings.
|
||||
*/
|
||||
@NonNull
|
||||
public static List<String> search(@NonNull String query) {
|
||||
if (query.isEmpty()) return Collections.emptyList();
|
||||
String lowerQuery = query.toLowerCase();
|
||||
List<String> results = new ArrayList<>();
|
||||
for (Emoji emoji : EmojiManager.getAll()) {
|
||||
if (matchesQuery(emoji, lowerQuery)) {
|
||||
results.add(emoji.getUnicode());
|
||||
if (results.size() >= MAX_RESULTS) break;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private static boolean matchesQuery(@NonNull Emoji emoji, @NonNull String lowerQuery) {
|
||||
for (String alias : emoji.getAliases()) {
|
||||
if (alias.toLowerCase().contains(lowerQuery)) return true;
|
||||
}
|
||||
for (String tag : emoji.getTags()) {
|
||||
if (tag.toLowerCase().contains(lowerQuery)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* RecyclerView adapter for showing emoji auto-completion suggestions.
|
||||
*/
|
||||
public class EmojiSuggestionAdapter extends RecyclerView.Adapter<EmojiSuggestionAdapter.EmojiViewHolder> {
|
||||
|
||||
public interface OnEmojiClickListener {
|
||||
void onEmojiClicked(@NonNull String emoji);
|
||||
}
|
||||
|
||||
private final List<String> emojis = new ArrayList<>();
|
||||
private OnEmojiClickListener listener;
|
||||
|
||||
public void setOnEmojiClickListener(OnEmojiClickListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setEmojis(@NonNull List<String> newEmojis) {
|
||||
int oldSize = emojis.size();
|
||||
emojis.clear();
|
||||
emojis.addAll(newEmojis);
|
||||
int newSize = emojis.size();
|
||||
if (oldSize > 0) notifyItemRangeRemoved(0, oldSize);
|
||||
if (newSize > 0) notifyItemRangeInserted(0, newSize);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public EmojiViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.emoji_suggestion_item, parent, false);
|
||||
return new EmojiViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull EmojiViewHolder holder, int position) {
|
||||
String emoji = emojis.get(position);
|
||||
holder.emojiView.setText(emoji);
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (listener != null) listener.onEmojiClicked(emoji);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return emojis.size();
|
||||
}
|
||||
|
||||
static class EmojiViewHolder extends RecyclerView.ViewHolder {
|
||||
final TextView emojiView;
|
||||
|
||||
EmojiViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
emojiView = itemView.findViewById(R.id.emoji_text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,11 +42,24 @@
|
||||
app:layout_constraintTop_toBottomOf="@id/quote_view"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/emoji_suggestions"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:background="?attr/input_panel_bg_color"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/subject_text" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/input_field_frame_layout"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/button_toggle"
|
||||
app:layout_constraintTop_toBottomOf="@id/subject_text"
|
||||
app:layout_constraintTop_toBottomOf="@id/emoji_suggestions"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/emoji_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:minWidth="44dp"
|
||||
android:textSize="26sp"
|
||||
android:textColor="?attr/emoji_text_color"
|
||||
android:background="@drawable/touch_highlight_background" />
|
||||
Reference in New Issue
Block a user