Autocomplete @ mentions in the composer. Closes #103
This commit is contained in:
parent
8994d81c66
commit
74aa866647
5 changed files with 168 additions and 8 deletions
|
@ -18,6 +18,7 @@ package com.keylesspalace.tusky;
|
|||
import android.Manifest;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
@ -39,6 +40,7 @@ import android.os.Parcel;
|
|||
import android.os.Parcelable;
|
||||
import android.provider.MediaStore;
|
||||
import android.support.annotation.AttrRes;
|
||||
import android.support.annotation.LayoutRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.design.widget.Snackbar;
|
||||
|
@ -60,14 +62,18 @@ import android.util.Log;
|
|||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Filter;
|
||||
import android.widget.Filterable;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.entity.Media;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.fragment.ComposeOptionsFragment;
|
||||
|
@ -75,6 +81,7 @@ import com.keylesspalace.tusky.util.CountUpDownLatch;
|
|||
import com.keylesspalace.tusky.util.DownsizeImageTask;
|
||||
import com.keylesspalace.tusky.util.IOUtils;
|
||||
import com.keylesspalace.tusky.util.MediaUtils;
|
||||
import com.keylesspalace.tusky.util.MentionTokenizer;
|
||||
import com.keylesspalace.tusky.util.ParserUtils;
|
||||
import com.keylesspalace.tusky.util.SpanUtils;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
|
@ -313,6 +320,10 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
|||
}
|
||||
});
|
||||
|
||||
textEditor.setAdapter(new MentionAutoCompleteAdapter(this,
|
||||
android.R.layout.simple_dropdown_item_1line));
|
||||
textEditor.setTokenizer(new MentionTokenizer());
|
||||
|
||||
// Add any mentions to the text field when a reply is first composed.
|
||||
if (mentionedUsernames != null) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
@ -1220,7 +1231,6 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// remove the precedent paste from the edit text
|
||||
private void cleanBaseUrl(ParserUtils.HeaderInfo headerInfo) {
|
||||
int lengthBaseUrl = headerInfo.baseUrl.length();
|
||||
|
@ -1236,6 +1246,28 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
|||
displayTransientError(R.string.error_generic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a synchronous search request for usernames fulfilling the given partial mention text.
|
||||
*/
|
||||
private ArrayList<String> autocompleteMention(String mention) {
|
||||
ArrayList<String> resultList = new ArrayList<>();
|
||||
try {
|
||||
List<Account> accountList = mastodonAPI.searchAccounts(mention, false, 5)
|
||||
.execute()
|
||||
.body();
|
||||
/* Match only accounts whose username contains the partial mention text, because
|
||||
searches also return matches for display names, which aren't relevant here. */
|
||||
for (Account account : accountList) {
|
||||
if (account.username.toLowerCase().contains(mention.toLowerCase())) {
|
||||
resultList.add(account.username);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, String.format("Autocomplete search for %s failed.", mention));
|
||||
}
|
||||
return resultList;
|
||||
}
|
||||
|
||||
private static class QueuedMedia {
|
||||
Type type;
|
||||
ImageView preview;
|
||||
|
@ -1311,4 +1343,47 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
|||
dest.writeLong(mediaSize);
|
||||
}
|
||||
}
|
||||
|
||||
private class MentionAutoCompleteAdapter extends ArrayAdapter<String> implements Filterable {
|
||||
private ArrayList<String> resultList;
|
||||
|
||||
MentionAutoCompleteAdapter(Context context, @LayoutRes int resource) {
|
||||
super(context, resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return resultList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getItem(int index) {
|
||||
return resultList.get(index);
|
||||
}
|
||||
|
||||
@Override @NonNull
|
||||
public Filter getFilter() {
|
||||
return new Filter() {
|
||||
@Override
|
||||
protected FilterResults performFiltering(CharSequence constraint) {
|
||||
FilterResults filterResults = new FilterResults();
|
||||
if (constraint != null) {
|
||||
resultList = autocompleteMention(constraint.toString());
|
||||
filterResults.values = resultList;
|
||||
filterResults.count = resultList.size();
|
||||
}
|
||||
return filterResults;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void publishResults(CharSequence constraint, FilterResults results) {
|
||||
if (results != null && results.count > 0) {
|
||||
notifyDataSetChanged();
|
||||
} else {
|
||||
notifyDataSetInvalidated();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,18 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.util;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.util;
|
||||
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.MultiAutoCompleteTextView;
|
||||
|
||||
public class MentionTokenizer implements MultiAutoCompleteTextView.Tokenizer {
|
||||
@Override
|
||||
public int findTokenStart(CharSequence text, int cursor) {
|
||||
int i = cursor;
|
||||
while (i > 0 && text.charAt(i - 1) != '@') {
|
||||
i--;
|
||||
}
|
||||
if (i < 1 || text.charAt(i - 1) != '@') {
|
||||
return cursor;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findTokenEnd(CharSequence text, int cursor) {
|
||||
int i = cursor;
|
||||
int length = text.length();
|
||||
while (i < length) {
|
||||
if (text.charAt(i) == ' ') {
|
||||
return i;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence terminateToken(CharSequence text) {
|
||||
int i = text.length();
|
||||
while (i > 0 && text.charAt(i - 1) == ' ') {
|
||||
i--;
|
||||
}
|
||||
if (i > 0 && text.charAt(i - 1) == ' ') {
|
||||
return text;
|
||||
} else if (text instanceof Spanned) {
|
||||
SpannableString s = new SpannableString(text + " ");
|
||||
TextUtils.copySpansFrom((Spanned) text, 0, text.length(), Object.class, s, 0);
|
||||
return s;
|
||||
} else {
|
||||
return text + " ";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,18 +18,18 @@ package com.keylesspalace.tusky.view;
|
|||
import android.content.Context;
|
||||
import android.support.v13.view.inputmethod.EditorInfoCompat;
|
||||
import android.support.v13.view.inputmethod.InputConnectionCompat;
|
||||
import android.support.v7.widget.AppCompatEditText;
|
||||
import android.support.v7.widget.AppCompatMultiAutoCompleteTextView;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
import com.keylesspalace.tusky.util.Assert;
|
||||
|
||||
public class EditTextTyped extends AppCompatEditText {
|
||||
public class EditTextTyped extends AppCompatMultiAutoCompleteTextView {
|
||||
|
||||
InputConnectionCompat.OnCommitContentListener onCommitContentListener;
|
||||
String[] mimeTypes;
|
||||
private OnPasteListener mOnPasteListener;
|
||||
private OnPasteListener onPasteListener;
|
||||
|
||||
public EditTextTyped(Context context) {
|
||||
super(context);
|
||||
|
@ -40,7 +40,7 @@ public class EditTextTyped extends AppCompatEditText {
|
|||
}
|
||||
|
||||
public void addOnPasteListener(OnPasteListener mOnPasteListener) {
|
||||
this.mOnPasteListener = mOnPasteListener;
|
||||
this.onPasteListener = mOnPasteListener;
|
||||
}
|
||||
|
||||
public void setMimeTypes(String[] types,
|
||||
|
@ -68,6 +68,7 @@ public class EditTextTyped extends AppCompatEditText {
|
|||
switch (id) {
|
||||
case android.R.id.paste:
|
||||
onPaste();
|
||||
break;
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
@ -76,8 +77,9 @@ public class EditTextTyped extends AppCompatEditText {
|
|||
* Text was pasted into the EditText.
|
||||
*/
|
||||
public void onPaste() {
|
||||
if (mOnPasteListener != null)
|
||||
mOnPasteListener.onPaste();
|
||||
if (onPasteListener != null) {
|
||||
onPasteListener.onPaste();
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnPasteListener {
|
||||
|
|
|
@ -62,7 +62,9 @@
|
|||
android:ems="10"
|
||||
android:gravity="start|top"
|
||||
android:hint="@string/hint_compose"
|
||||
android:inputType="text|textMultiLine|textCapSentences" />
|
||||
android:inputType="text|textMultiLine|textCapSentences"
|
||||
android:dropDownWidth="wrap_content"
|
||||
android:completionThreshold="2" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
|
|
Loading…
Reference in a new issue