mirror of
https://codeberg.org/monocles/monocles_chat.git
synced 2025-01-15 22:22:22 +01:00
@mention autocomplete UI
This commit is contained in:
parent
a2a694c1bb
commit
40b0ba7838
5 changed files with 149 additions and 31 deletions
|
@ -76,8 +76,8 @@ dependencies {
|
|||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.7'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.emoji2:emoji2:1.4.0'
|
||||
gitImplementation "androidx.emoji2:emoji2-bundled:1.4.0"
|
||||
implementation 'androidx.emoji2:emoji2:1.5.0'
|
||||
gitImplementation "androidx.emoji2:emoji2-bundled:1.5.0"
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0' // for compatibility
|
||||
|
@ -126,14 +126,15 @@ dependencies {
|
|||
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
|
||||
implementation 'com.nineoldandroids:library:2.4.0'
|
||||
implementation "androidx.core:core-ktx:1.13.1"
|
||||
implementation "androidx.compose.material3:material3-android:1.2.1"
|
||||
implementation "androidx.emoji2:emoji2-emojipicker:1.4.0"
|
||||
implementation "androidx.compose.material3:material3-android:1.3.0"
|
||||
implementation "androidx.emoji2:emoji2-emojipicker:1.5.0"
|
||||
implementation 'com.github.Priyansh-Kedia:OpenGraphParser:2.5.6'
|
||||
implementation 'com.github.bumptech.glide:glide:4.15.1' // For photo editor compatibility
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'com.github.inusedname:AndroidPhotoFilters:1.0.7.2'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
implementation 'com.github.natario1:Autocomplete:v1.1.0'
|
||||
}
|
||||
|
||||
ext {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package eu.siacs.conversations.entities;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
|
@ -408,6 +409,18 @@ public class MucOptions {
|
|||
}
|
||||
}
|
||||
|
||||
public ArrayList<User> getUsersByRole(Role role) {
|
||||
synchronized (users) {
|
||||
ArrayList<User> list = new ArrayList<>();
|
||||
for (User user : users) {
|
||||
if (user.getRole().ranks(role)) {
|
||||
list.add(user);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
public ArrayList<User> getUsersWithChatState(ChatState state, int max) {
|
||||
synchronized (users) {
|
||||
ArrayList<User> list = new ArrayList<>();
|
||||
|
@ -896,6 +909,17 @@ public class MucOptions {
|
|||
return this.hats == null ? new HashSet<>() : hats;
|
||||
}
|
||||
|
||||
public List<MucOptions.Hat> getPseudoHats(Context context) {
|
||||
List<MucOptions.Hat> hats = new ArrayList<>();
|
||||
if (getAffiliation() != MucOptions.Affiliation.NONE) {
|
||||
hats.add(new MucOptions.Hat(null, context.getString(getAffiliation().getResId())));
|
||||
}
|
||||
if (getRole() != MucOptions.Role.PARTICIPANT) {
|
||||
hats.add(new MucOptions.Hat(null, context.getString(getRole().getResId())));
|
||||
}
|
||||
return hats;
|
||||
}
|
||||
|
||||
public long getPgpKeyId() {
|
||||
if (this.pgpKeyId != 0) {
|
||||
return this.pgpKeyId;
|
||||
|
@ -997,6 +1021,14 @@ public class MucOptions {
|
|||
|
||||
@Override
|
||||
public int compareTo(@NonNull User another) {
|
||||
final var anotherPseudoId = another.getOccupantId() != null && another.getOccupantId().charAt(0) == '\0';
|
||||
final var pseudoId = getOccupantId() != null && getOccupantId().charAt(0) == '\0';
|
||||
if (anotherPseudoId && !pseudoId) {
|
||||
return 1;
|
||||
}
|
||||
if (pseudoId && !anotherPseudoId) {
|
||||
return -1;
|
||||
}
|
||||
if (another.getAffiliation().outranks(getAffiliation())) {
|
||||
return 1;
|
||||
} else if (getAffiliation().outranks(another.getAffiliation())) {
|
||||
|
|
|
@ -115,13 +115,21 @@ import androidx.core.view.inputmethod.InputContentInfoCompat;
|
|||
import androidx.databinding.DataBindingUtil;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.emoji2.emojipicker.EmojiPickerView;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.google.android.material.materialswitch.MaterialSwitch;
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.collect.Collections2;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.otaliastudios.autocomplete.Autocomplete;
|
||||
import com.otaliastudios.autocomplete.AutocompleteCallback;
|
||||
import com.otaliastudios.autocomplete.AutocompletePresenter;
|
||||
import com.otaliastudios.autocomplete.CharPolicy;
|
||||
import com.otaliastudios.autocomplete.RecyclerViewPresenter;
|
||||
|
||||
import net.java.otr4j.session.SessionStatus;
|
||||
|
||||
|
@ -155,6 +163,7 @@ import java.util.concurrent.TimeUnit;
|
|||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import de.monocles.chat.BobTransfer;
|
||||
|
@ -193,6 +202,7 @@ import eu.siacs.conversations.ui.adapter.CommandAdapter;
|
|||
import eu.siacs.conversations.ui.adapter.MediaPreviewAdapter;
|
||||
import eu.siacs.conversations.ui.adapter.MessageAdapter;
|
||||
import eu.siacs.conversations.ui.adapter.MessageLogAdapter;
|
||||
import eu.siacs.conversations.ui.adapter.UserAdapter;
|
||||
import eu.siacs.conversations.ui.adapter.model.MessageLogModel;
|
||||
import eu.siacs.conversations.ui.util.ActivityResult;
|
||||
import eu.siacs.conversations.ui.util.Attachment;
|
||||
|
@ -1425,18 +1435,6 @@ public class ConversationFragment extends XmppFragment
|
|||
body.delete(0, 6);
|
||||
while (body.length() > 0 && Character.isWhitespace(body.charAt(0))) body.delete(0, 1);
|
||||
}
|
||||
if (Pattern.compile("\\A@mods\\s.*").matcher(body).find()) {
|
||||
body.delete(0, 5);
|
||||
final var mods = new StringBuffer();
|
||||
for (final var user : conversation.getMucOptions().getUsers()) {
|
||||
if (user.getRole().ranks(MucOptions.Role.MODERATOR)) {
|
||||
if (mods.length() > 0) mods.append(", ");
|
||||
mods.append(user.getNick());
|
||||
}
|
||||
}
|
||||
mods.append(":");
|
||||
body.insert(0, mods.toString());
|
||||
}
|
||||
if (conversation.getReplyTo() != null) {
|
||||
if (Emoticons.isEmoji(body.toString().replaceAll("\\s", ""))) {
|
||||
message = conversation.getReplyTo().react(body.toString().replaceAll("\\s", ""));
|
||||
|
@ -2036,6 +2034,104 @@ public class ConversationFragment extends XmppFragment
|
|||
return true;
|
||||
});
|
||||
|
||||
Autocomplete.<MucOptions.User>on(binding.textinput)
|
||||
.with(activity.getDrawable(R.drawable.input_bubble_light))
|
||||
.with(new CharPolicy('@'))
|
||||
.with(new RecyclerViewPresenter<User>(activity) {
|
||||
protected UserAdapter adapter;
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter instantiateAdapter() {
|
||||
adapter = new UserAdapter(false) {
|
||||
@Override
|
||||
public void onBindViewHolder(UserAdapter.ViewHolder viewHolder, int position) {
|
||||
super.onBindViewHolder(viewHolder, position);
|
||||
final var item = getItem(position);
|
||||
viewHolder.binding.getRoot().setOnClickListener(v -> {
|
||||
dispatchClick(item);
|
||||
});
|
||||
}
|
||||
};
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onQuery(@Nullable CharSequence query) {
|
||||
getRecyclerView().getItemAnimator().endAnimations();
|
||||
final var allUsers = conversation.getMucOptions().getUsers();
|
||||
if (!conversation.getMucOptions().getUsersByRole(MucOptions.Role.MODERATOR).isEmpty()) {
|
||||
final var u = new MucOptions.User(conversation.getMucOptions(), null, "\0role:moderator", "Notify active moderators", new HashSet<>());
|
||||
u.setRole("participant");
|
||||
allUsers.add(u);
|
||||
}
|
||||
if (!allUsers.isEmpty()) {
|
||||
final var u = new MucOptions.User(conversation.getMucOptions(), null, "\0attention", "Notify active participants", new HashSet<>());
|
||||
u.setRole("participant");
|
||||
allUsers.add(u);
|
||||
}
|
||||
final String needle = query.toString().toLowerCase(Locale.getDefault());
|
||||
adapter.submitList(
|
||||
Ordering.natural().immutableSortedCopy(Collections2.filter(
|
||||
allUsers,
|
||||
user -> {
|
||||
if ("mods".contains(needle) && "\0role:moderator".equals(user.getOccupantId())) return true;
|
||||
if ("here".contains(needle) && "\0attention".equals(user.getOccupantId())) return true;
|
||||
final String name = user.getNick();
|
||||
if (name == null) return false;
|
||||
for (final var hat : user.getHats()) {
|
||||
if (hat.toString().toLowerCase(Locale.getDefault()).contains(needle)) return true;
|
||||
}
|
||||
for (final var hat : user.getPseudoHats(activity)) {
|
||||
if (hat.toString().toLowerCase(Locale.getDefault()).contains(needle)) return true;
|
||||
}
|
||||
final Contact contact = user.getContact();
|
||||
return name.toLowerCase(Locale.getDefault()).contains(needle)
|
||||
|| contact != null
|
||||
&& contact.getDisplayName().toLowerCase(Locale.getDefault()).contains(needle);
|
||||
})));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AutocompletePresenter.PopupDimensions getPopupDimensions() {
|
||||
final var dim = new AutocompletePresenter.PopupDimensions();
|
||||
dim.width = displayMetrics.widthPixels * 4/5;
|
||||
return dim;
|
||||
}
|
||||
})
|
||||
.with(new AutocompleteCallback<User>() {
|
||||
@Override
|
||||
public boolean onPopupItemClicked(Editable editable, MucOptions.User user) {
|
||||
int[] range = com.otaliastudios.autocomplete.CharPolicy.getQueryRange(editable);
|
||||
if (range == null) return false;
|
||||
range[0] -= 1;
|
||||
if ("\0attention".equals(user.getOccupantId())) {
|
||||
editable.delete(Math.max(0, range[0]), Math.min(editable.length(), range[1]));
|
||||
editable.insert(0, "@here ");
|
||||
return true;
|
||||
}
|
||||
int colon = editable.toString().indexOf(':');
|
||||
final var beforeColon = range[0] < colon;
|
||||
String prefix = "";
|
||||
String suffix = " ";
|
||||
if (beforeColon) suffix = ", ";
|
||||
if (colon < 0 && range[0] == 0) suffix = ": ";
|
||||
if (colon > 0 && colon == range[0] - 2) {
|
||||
prefix = ", ";
|
||||
suffix = ": ";
|
||||
range[0] -= 2;
|
||||
}
|
||||
var insert = user.getNick();
|
||||
if ("\0role:moderator".equals(user.getOccupantId())) {
|
||||
insert = conversation.getMucOptions().getUsersByRole(MucOptions.Role.MODERATOR).stream().map(MucOptions.User::getNick).collect(Collectors.joining(", "));
|
||||
}
|
||||
editable.replace(Math.max(0, range[0]), Math.min(editable.length(), range[1]), prefix + insert + suffix);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPopupVisibilityChanged(boolean shown) {}
|
||||
}).build();
|
||||
|
||||
hasWriteAccessInMUC();
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ public class ChannelSearchResultAdapter extends ListAdapter<Room, ChannelSearchR
|
|||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final SearchResultItemBinding binding;
|
||||
public final SearchResultItemBinding binding;
|
||||
|
||||
private ViewHolder(SearchResultItemBinding binding) {
|
||||
super(binding.getRoot());
|
||||
|
|
|
@ -126,7 +126,7 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
|
|||
|
||||
viewHolder.binding.tags.setVisibility(View.VISIBLE);
|
||||
viewHolder.binding.tags.removeAllViewsInLayout();
|
||||
for (MucOptions.Hat hat : getPseudoHats(viewHolder.binding.getRoot().getContext(), user)) {
|
||||
for (MucOptions.Hat hat : user.getPseudoHats(viewHolder.binding.getRoot().getContext())) {
|
||||
TextView tv = (TextView) LayoutInflater.from(viewHolder.binding.getRoot().getContext()).inflate(R.layout.list_item_tag, viewHolder.binding.tags, false);
|
||||
tv.setText(hat.toString());
|
||||
Drawable unwrappedDrawable = AppCompatResources.getDrawable(tv.getContext(), R.drawable.rounded_tag);
|
||||
|
@ -151,17 +151,6 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
|
|||
}
|
||||
}
|
||||
|
||||
private List<MucOptions.Hat> getPseudoHats(Context context, MucOptions.User user) {
|
||||
List<MucOptions.Hat> hats = new ArrayList<>();
|
||||
if (user.getAffiliation() != MucOptions.Affiliation.NONE) {
|
||||
hats.add(new MucOptions.Hat(null, context.getString(user.getAffiliation().getResId())));
|
||||
}
|
||||
if (user.getRole() != MucOptions.Role.PARTICIPANT) {
|
||||
hats.add(new MucOptions.Hat(null, context.getString(user.getRole().getResId())));
|
||||
}
|
||||
return hats;
|
||||
}
|
||||
|
||||
public MucOptions.User getSelectedUser() {
|
||||
return selectedUser;
|
||||
}
|
||||
|
@ -171,9 +160,9 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
|
|||
MucDetailsContextMenuHelper.onCreateContextMenu(menu, v);
|
||||
}
|
||||
|
||||
class ViewHolder extends RecyclerView.ViewHolder {
|
||||
protected class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ContactBinding binding;
|
||||
public final ContactBinding binding;
|
||||
|
||||
private ViewHolder(ContactBinding binding) {
|
||||
super(binding.getRoot());
|
||||
|
|
Loading…
Reference in a new issue