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.Manifest;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
@ -39,6 +40,7 @@ import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.support.annotation.AttrRes;
|
import android.support.annotation.AttrRes;
|
||||||
|
import android.support.annotation.LayoutRes;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.StringRes;
|
import android.support.annotation.StringRes;
|
||||||
import android.support.design.widget.Snackbar;
|
import android.support.design.widget.Snackbar;
|
||||||
|
@ -60,14 +62,18 @@ import android.util.Log;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
|
import android.widget.Filter;
|
||||||
|
import android.widget.Filterable;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.entity.Account;
|
||||||
import com.keylesspalace.tusky.entity.Media;
|
import com.keylesspalace.tusky.entity.Media;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.fragment.ComposeOptionsFragment;
|
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.DownsizeImageTask;
|
||||||
import com.keylesspalace.tusky.util.IOUtils;
|
import com.keylesspalace.tusky.util.IOUtils;
|
||||||
import com.keylesspalace.tusky.util.MediaUtils;
|
import com.keylesspalace.tusky.util.MediaUtils;
|
||||||
|
import com.keylesspalace.tusky.util.MentionTokenizer;
|
||||||
import com.keylesspalace.tusky.util.ParserUtils;
|
import com.keylesspalace.tusky.util.ParserUtils;
|
||||||
import com.keylesspalace.tusky.util.SpanUtils;
|
import com.keylesspalace.tusky.util.SpanUtils;
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
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.
|
// Add any mentions to the text field when a reply is first composed.
|
||||||
if (mentionedUsernames != null) {
|
if (mentionedUsernames != null) {
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
|
@ -1220,7 +1231,6 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// remove the precedent paste from the edit text
|
// remove the precedent paste from the edit text
|
||||||
private void cleanBaseUrl(ParserUtils.HeaderInfo headerInfo) {
|
private void cleanBaseUrl(ParserUtils.HeaderInfo headerInfo) {
|
||||||
int lengthBaseUrl = headerInfo.baseUrl.length();
|
int lengthBaseUrl = headerInfo.baseUrl.length();
|
||||||
|
@ -1236,6 +1246,28 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
||||||
displayTransientError(R.string.error_generic);
|
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 {
|
private static class QueuedMedia {
|
||||||
Type type;
|
Type type;
|
||||||
ImageView preview;
|
ImageView preview;
|
||||||
|
@ -1311,4 +1343,47 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
||||||
dest.writeLong(mediaSize);
|
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;
|
package com.keylesspalace.tusky.util;
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
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.content.Context;
|
||||||
import android.support.v13.view.inputmethod.EditorInfoCompat;
|
import android.support.v13.view.inputmethod.EditorInfoCompat;
|
||||||
import android.support.v13.view.inputmethod.InputConnectionCompat;
|
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.util.AttributeSet;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
import android.view.inputmethod.InputConnection;
|
import android.view.inputmethod.InputConnection;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.util.Assert;
|
import com.keylesspalace.tusky.util.Assert;
|
||||||
|
|
||||||
public class EditTextTyped extends AppCompatEditText {
|
public class EditTextTyped extends AppCompatMultiAutoCompleteTextView {
|
||||||
|
|
||||||
InputConnectionCompat.OnCommitContentListener onCommitContentListener;
|
InputConnectionCompat.OnCommitContentListener onCommitContentListener;
|
||||||
String[] mimeTypes;
|
String[] mimeTypes;
|
||||||
private OnPasteListener mOnPasteListener;
|
private OnPasteListener onPasteListener;
|
||||||
|
|
||||||
public EditTextTyped(Context context) {
|
public EditTextTyped(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
|
@ -40,7 +40,7 @@ public class EditTextTyped extends AppCompatEditText {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addOnPasteListener(OnPasteListener mOnPasteListener) {
|
public void addOnPasteListener(OnPasteListener mOnPasteListener) {
|
||||||
this.mOnPasteListener = mOnPasteListener;
|
this.onPasteListener = mOnPasteListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMimeTypes(String[] types,
|
public void setMimeTypes(String[] types,
|
||||||
|
@ -68,6 +68,7 @@ public class EditTextTyped extends AppCompatEditText {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case android.R.id.paste:
|
case android.R.id.paste:
|
||||||
onPaste();
|
onPaste();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return consumed;
|
return consumed;
|
||||||
}
|
}
|
||||||
|
@ -76,8 +77,9 @@ public class EditTextTyped extends AppCompatEditText {
|
||||||
* Text was pasted into the EditText.
|
* Text was pasted into the EditText.
|
||||||
*/
|
*/
|
||||||
public void onPaste() {
|
public void onPaste() {
|
||||||
if (mOnPasteListener != null)
|
if (onPasteListener != null) {
|
||||||
mOnPasteListener.onPaste();
|
onPasteListener.onPaste();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface OnPasteListener {
|
public interface OnPasteListener {
|
||||||
|
|
|
@ -62,7 +62,9 @@
|
||||||
android:ems="10"
|
android:ems="10"
|
||||||
android:gravity="start|top"
|
android:gravity="start|top"
|
||||||
android:hint="@string/hint_compose"
|
android:hint="@string/hint_compose"
|
||||||
android:inputType="text|textMultiLine|textCapSentences" />
|
android:inputType="text|textMultiLine|textCapSentences"
|
||||||
|
android:dropDownWidth="wrap_content"
|
||||||
|
android:completionThreshold="2" />
|
||||||
|
|
||||||
<HorizontalScrollView
|
<HorizontalScrollView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
Loading…
Reference in a new issue