forked from mirror/monocles_chat_clean
@mention autocomplete UI
(cherry picked from commit 735e5c4aec55ec070018c3a08d2aba15e24ea897)
This commit is contained in:
parent
e5d1328337
commit
963de2bb2a
4 changed files with 164 additions and 40 deletions
27
build.gradle
27
build.gradle
|
@ -52,9 +52,13 @@ dependencies {
|
|||
implementation "androidx.core:core:1.10.1"
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
|
||||
implementation project(':libs:annotation')
|
||||
annotationProcessor project(':libs:annotation-processor')
|
||||
|
||||
|
||||
implementation 'androidx.viewpager:viewpager:1.0.0'
|
||||
|
||||
playstoreImplementation('com.google.firebase:firebase-messaging:23.4.1') {
|
||||
playstoreImplementation('com.google.firebase:firebase-messaging:24.0.1') {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||
|
@ -62,24 +66,22 @@ dependencies {
|
|||
monocleschatPlaystoreImplementation("com.android.installreferrer:installreferrer:2.2")
|
||||
monocleschatPlaystoreImplementation 'com.github.singpolyma:play-licensing:1c637ea03c'
|
||||
conversationsPlaystoreImplementation("com.android.installreferrer:installreferrer:2.2")
|
||||
quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.2'
|
||||
quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.1.0'
|
||||
implementation 'com.github.open-keychain.open-keychain:openpgp-api:v5.7.1'
|
||||
implementation("com.github.CanHub:Android-Image-Cropper:2.0.0")
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.7'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation "androidx.preference:preference:1.2.1"
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'androidx.work:work-runtime:2.9.0'
|
||||
|
||||
implementation "androidx.emoji2:emoji2:1.4.0"
|
||||
freeImplementation "androidx.emoji2:emoji2-bundled:1.4.0"
|
||||
|
||||
implementation 'org.bouncycastle:bcmail-jdk15on:1.64'
|
||||
//zxing stopped supporting Java 7 so we have to stick with 3.3.3
|
||||
//https://github.com/zxing/zxing/issues/1170
|
||||
implementation 'com.google.zxing:core:3.3.3'
|
||||
implementation 'org.bouncycastle:bcmail-jdk18on:1.78.1'
|
||||
implementation 'com.google.zxing:core:3.5.3'
|
||||
implementation 'org.minidns:minidns-hla:1.0.5'
|
||||
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
||||
implementation 'org.whispersystems:signal-protocol-java:2.6.2'
|
||||
|
@ -99,12 +101,12 @@ dependencies {
|
|||
implementation 'me.drakeet.support:toastcompat:1.1.0'
|
||||
implementation "com.leinardi.android:speed-dial:3.3.0"
|
||||
|
||||
implementation "com.squareup.retrofit2:retrofit:2.9.0"
|
||||
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
|
||||
implementation "com.squareup.retrofit2:retrofit:2.11.0"
|
||||
implementation "com.squareup.retrofit2:converter-gson:2.11.0"
|
||||
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
||||
|
||||
implementation 'com.google.guava:guava:32.1.3-android'
|
||||
implementation 'io.michaelrocks:libphonenumber-android:8.13.28'
|
||||
implementation 'io.michaelrocks:libphonenumber-android:8.13.35'
|
||||
implementation 'im.conversations.webrtc:webrtc-android:119.0.1'
|
||||
implementation 'io.github.nishkarsh:android-permissions:2.1.6'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
|
@ -128,6 +130,7 @@ dependencies {
|
|||
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
|
||||
implementation 'com.google.android.exoplayer:extension-mediasession:2.19.1'
|
||||
implementation 'com.github.natario1:Autocomplete:v1.1.0'
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -307,7 +310,7 @@ android {
|
|||
}
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += ['META-INF/BCKEY.DSA', 'META-INF/BCKEY.SF']
|
||||
excludes += ['META-INF/BCKEY.DSA', 'META-INF/BCKEY.SF', 'META-INF/versions/9/OSGI-INF/MANIFEST.MF']
|
||||
}
|
||||
}
|
||||
lint {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package eu.siacs.conversations.entities;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
|
@ -410,6 +411,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<>();
|
||||
|
@ -923,6 +936,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;
|
||||
|
@ -1026,6 +1050,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())) {
|
||||
|
|
|
@ -92,6 +92,7 @@ import androidx.core.view.inputmethod.InputConnectionCompat;
|
|||
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
||||
import androidx.databinding.DataBindingUtil;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
|
@ -105,8 +106,17 @@ import de.monocles.chat.WebxdcStore;
|
|||
import com.google.android.material.color.MaterialColors;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
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.Lists;
|
||||
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 org.jetbrains.annotations.NotNull;
|
||||
|
||||
|
@ -137,6 +147,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 eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
|
@ -167,6 +178,7 @@ import eu.siacs.conversations.services.XmppConnectionService;
|
|||
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.UserAdapter;
|
||||
import eu.siacs.conversations.ui.util.ActivityResult;
|
||||
import eu.siacs.conversations.ui.util.Attachment;
|
||||
import eu.siacs.conversations.ui.util.ConversationMenuConfigurator;
|
||||
|
@ -208,6 +220,8 @@ import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
|
|||
import eu.siacs.conversations.xmpp.jingle.RtpCapability;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
|
||||
import im.conversations.android.xmpp.model.stanza.Iq;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -1065,18 +1079,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", ""));
|
||||
|
@ -1611,6 +1613,104 @@ public class ConversationFragment extends XmppFragment
|
|||
return true;
|
||||
});
|
||||
|
||||
Autocomplete.<MucOptions.User>on(binding.textinput)
|
||||
.with(activity.getDrawable(R.drawable.background_message_bubble))
|
||||
.with(new CharPolicy('@'))
|
||||
.with(new RecyclerViewPresenter<MucOptions.User>(activity) {
|
||||
protected UserAdapter adapter;
|
||||
|
||||
@Override
|
||||
protected 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<MucOptions.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();
|
||||
|
||||
final Pattern lastColonPattern = Pattern.compile("(?<!\\w):");
|
||||
emojiSearchBinding = DataBindingUtil.inflate(inflater, R.layout.emoji_search, null, false);
|
||||
emojiSearchBinding.emoji.setOnItemClickListener((parent, view, position, id) -> {
|
||||
|
@ -3513,13 +3613,13 @@ public class ConversationFragment extends XmppFragment
|
|||
} else {
|
||||
if (!delayShow) conversation.showViewPager();
|
||||
binding.commandsViewProgressbar.setVisibility(View.VISIBLE);
|
||||
activity.xmppConnectionService.fetchCommands(conversation.getAccount(), commandJid, (a, iq) -> {
|
||||
activity.xmppConnectionService.fetchCommands(conversation.getAccount(), commandJid, (iq) -> {
|
||||
if (activity == null) return;
|
||||
|
||||
activity.runOnUiThread(() -> {
|
||||
binding.commandsViewProgressbar.setVisibility(View.GONE);
|
||||
commandAdapter.clear();
|
||||
if (iq.getType() == IqPacket.TYPE.RESULT) {
|
||||
if (iq.getType() == Iq.Type.RESULT) {
|
||||
for (Element child : iq.query().getChildren()) {
|
||||
if (!"item".equals(child.getName()) || !Namespace.DISCO_ITEMS.equals(child.getNamespace())) continue;
|
||||
commandAdapter.add(new CommandAdapter.Command0050(child));
|
||||
|
|
|
@ -147,7 +147,7 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
|
|||
viewHolder.binding.tags.setVisibility(View.VISIBLE);
|
||||
viewHolder.binding.tags.removeViews(1, viewHolder.binding.tags.getChildCount() - 1);
|
||||
final ImmutableList.Builder<Integer> viewIdBuilder = new ImmutableList.Builder<>();
|
||||
for (MucOptions.Hat hat : getPseudoHats(viewHolder.binding.getRoot().getContext(), user)) {
|
||||
for (MucOptions.Hat hat : user.getPseudoHats(viewHolder.binding.getRoot().getContext())) {
|
||||
final String tag = hat.toString();
|
||||
final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, viewHolder.binding.tags, false);
|
||||
tv.setText(tag);
|
||||
|
@ -175,17 +175,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;
|
||||
}
|
||||
|
@ -195,9 +184,9 @@ public class UserAdapter extends ListAdapter<MucOptions.User, UserAdapter.ViewHo
|
|||
MucDetailsContextMenuHelper.onCreateContextMenu(menu,v);
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ItemContactBinding binding;
|
||||
public final ItemContactBinding binding;
|
||||
|
||||
private ViewHolder(ItemContactBinding binding) {
|
||||
super(binding.getRoot());
|
||||
|
|
Loading…
Reference in a new issue