Save reply info in draft, refactor (#449)

* Save reply info in draft, refactor

* Handle replying to deleted status
This commit is contained in:
Ivan Kupalov 2017-11-16 21:18:11 +03:00 committed by Konrad Pozniak
parent a859ef0432
commit 2575b16dad
15 changed files with 575 additions and 390 deletions

View file

@ -1,4 +1,5 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android'
android {
@ -65,13 +66,14 @@ dependencies {
compile 'com.evernote:android-job:1.2.0'
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
//room
compile 'android.arch.persistence.room:runtime:1.0.0'
annotationProcessor 'android.arch.persistence.room:compiler:1.0.0'
testCompile 'junit:junit:4.12'
implementation "android.arch.persistence.room:runtime:1.0.0"
kapt 'android.arch.persistence.room:compiler:1.0.0'
testCompile "junit:junit:4.12"
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
}
repositories {
mavenCentral()

View file

@ -16,6 +16,7 @@
package com.keylesspalace.tusky;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.Context;
@ -26,10 +27,7 @@ import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadataRetriever;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
@ -39,9 +37,9 @@ 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.Nullable;
import android.support.annotation.Px;
import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.support.v13.view.inputmethod.InputConnectionCompat;
@ -61,16 +59,11 @@ import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.style.URLSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
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;
@ -80,6 +73,7 @@ import android.widget.Toast;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.keylesspalace.tusky.adapter.MentionAutoCompleteAdapter;
import com.keylesspalace.tusky.db.TootDao;
import com.keylesspalace.tusky.db.TootEntity;
import com.keylesspalace.tusky.entity.Account;
@ -98,9 +92,6 @@ import com.keylesspalace.tusky.util.StringUtils;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.view.EditTextTyped;
import com.keylesspalace.tusky.view.ProgressImageView;
import com.keylesspalace.tusky.view.RoundedTransformation;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import java.io.File;
import java.io.FileNotFoundException;
@ -121,7 +112,8 @@ import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public final class ComposeActivity extends BaseActivity implements ComposeOptionsFragment.Listener {
public final class ComposeActivity extends BaseActivity
implements ComposeOptionsFragment.Listener, MentionAutoCompleteAdapter.AccountSearchProvider {
private static final String TAG = "ComposeActivity"; // logging tag
private static final int STATUS_CHARACTER_LIMIT = 500;
private static final int STATUS_MEDIA_SIZE_LIMIT = 8388608; // 8MiB
@ -129,7 +121,8 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
private static final int MEDIA_TAKE_PHOTO_RESULT = 2;
private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1;
private static final int COMPOSE_SUCCESS = -1;
private static final int THUMBNAIL_SIZE = 128; // pixels
@Px
private static final int THUMBNAIL_SIZE = 128;
private static final String SAVED_TOOT_UID_EXTRA = "saved_toot_uid";
private static final String SAVED_TOOT_TEXT_EXTRA = "saved_toot_text";
@ -140,8 +133,11 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
private static final String MENTIONED_USERNAMES_EXTRA = "netnioned_usernames";
private static final String REPLYING_STATUS_AUTHOR_USERNAME_EXTRA = "replying_author_nickname_extra";
private static final String REPLYING_STATUS_CONTENT_EXTRA = "replying_status_content";
private static final String REMEMBERED_VISIBILITY_PREF = "rememberedVisibilityNum";
private static TootDao tootDao = TuskyApplication.getDB().tootDao();
private TextView replyTextView;
private TextView replyContentTextView;
private EditTextTyped textEditor;
private LinearLayout mediaPreviewBar;
@ -160,27 +156,21 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
private ArrayList<QueuedMedia> mediaQueued;
private CountUpDownLatch waitForMediaLatch;
private boolean showMarkSensitive;
private String statusVisibility; // The current values of the options that will be applied
private Status.Visibility statusVisibility; // The current values of the options that will be applied
private boolean statusMarkSensitive; // to the status being composed.
private boolean statusHideText; //
private boolean statusHideText;
private boolean statusAlreadyInFlight; // to prevent duplicate sends by mashing the send button
private InputContentInfoCompat currentInputContentInfo;
private int currentFlags;
private Uri photoUploadUri;
private int savedTootUid = 0;
/**
* The Target object must be stored as a member field or method and cannot be an anonymous class otherwise this won't work as expected. The reason is that Picasso accepts this parameter as a weak memory reference. Because anonymous classes are eligible for garbage collection when there are no more references, the network request to fetch the image may finish after this anonymous class has already been reclaimed. See this Stack Overflow discussion for more details.
*/
@SuppressWarnings("FieldCanBeLocal")
private Target target;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_compose);
TextView replyTextView = findViewById(R.id.reply_tv);
replyTextView = findViewById(R.id.reply_tv);
replyContentTextView = findViewById(R.id.reply_content_tv);
textEditor = findViewById(R.id.compose_edit_field);
mediaPreviewBar = findViewById(R.id.compose_media_preview_bar);
@ -253,13 +243,16 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
* state. */
SharedPreferences preferences = getPrivatePreferences();
String startingVisibility;
Status.Visibility startingVisibility;
boolean startingHideText;
String startingContentWarning = null;
ArrayList<SavedQueuedMedia> savedMediaQueued = null;
if (savedInstanceState != null) {
showMarkSensitive = savedInstanceState.getBoolean("showMarkSensitive");
startingVisibility = savedInstanceState.getString("statusVisibility");
startingVisibility = Status.Visibility.byNum(
savedInstanceState.getInt("statusVisibility",
Status.Visibility.PUBLIC.getNum())
);
statusMarkSensitive = savedInstanceState.getBoolean("statusMarkSensitive");
startingHideText = savedInstanceState.getBoolean("statusHideText");
// Keep these until everything needed to put them in the queue is finished initializing.
@ -274,7 +267,10 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
photoUploadUri = savedInstanceState.getParcelable("photoUploadUri");
} else {
showMarkSensitive = false;
startingVisibility = preferences.getString("rememberedVisibility", "public");
startingVisibility = Status.Visibility.byNum(
preferences.getInt(REMEMBERED_VISIBILITY_PREF,
Status.Visibility.UNKNOWN.getNum())
);
statusMarkSensitive = false;
startingHideText = false;
photoUploadUri = null;
@ -287,37 +283,20 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
String[] mentionedUsernames = null;
ArrayList<String> loadedDraftMediaUris = null;
inReplyToId = null;
Status.Visibility replyVisibility = Status.Visibility.UNKNOWN;
if (intent != null) {
inReplyToId = intent.getStringExtra(IN_REPLY_TO_ID_EXTRA);
String replyVisibility = intent.getStringExtra(REPLY_VISIBILITY_EXTRA);
if (replyVisibility != null && startingVisibility != null) {
// Lowest possible visibility setting in response
if (startingVisibility.equals("direct") || replyVisibility.equals("direct")) {
startingVisibility = "direct";
} else if (startingVisibility.equals("private") || replyVisibility.equals("private")) {
startingVisibility = "private";
} else if (startingVisibility.equals("unlisted") || replyVisibility.equals("unlisted")) {
startingVisibility = "unlisted";
} else {
startingVisibility = replyVisibility;
}
}
replyVisibility = Status.Visibility.byNum(
intent.getIntExtra(REPLY_VISIBILITY_EXTRA, Status.Visibility.UNKNOWN.getNum())
);
mentionedUsernames = intent.getStringArrayExtra(MENTIONED_USERNAMES_EXTRA);
if (inReplyToId != null) {
startingHideText = !intent.getStringExtra(CONTENT_WARNING_EXTRA).equals("");
String contentWarning = intent.getStringExtra(CONTENT_WARNING_EXTRA);
if (contentWarning != null) {
startingHideText = !contentWarning.isEmpty();
if (startingHideText) {
startingContentWarning = intent.getStringExtra(CONTENT_WARNING_EXTRA);
}
} else {
String contentWarning = intent.getStringExtra(CONTENT_WARNING_EXTRA);
if (contentWarning != null) {
startingHideText = !contentWarning.isEmpty();
if (startingHideText) {
startingContentWarning = contentWarning;
}
startingContentWarning = contentWarning;
}
}
@ -361,14 +340,12 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
}
}
/* If the currently logged in account is locked, its posts should default to private. This
* should override even the reply settings, so this must be done after those are set up. */
if (preferences.getBoolean("loggedInAccountLocked", false)) {
startingVisibility = "private";
}
Status.Visibility pickedVisibility = pickVisibility(startingVisibility, replyVisibility,
preferences.getBoolean("loggedInAccountLocked", false));
// After the starting state is finalised, the interface can be set to reflect this state.
setStatusVisibility(startingVisibility);
setStatusVisibility(pickedVisibility);
postProgress.setVisibility(View.INVISIBLE);
updateHideMediaToggleColor();
updateVisibleCharactersLeft();
@ -393,7 +370,8 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
}
});
textEditor.setAdapter(new MentionAutoCompleteAdapter(this, R.layout.item_autocomplete));
textEditor.setAdapter(
new MentionAutoCompleteAdapter(this, R.layout.item_autocomplete, this));
textEditor.setTokenizer(new MentionTokenizer());
// Add any mentions to the text field when a reply is first composed.
@ -442,7 +420,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
}
} else if (savedMediaQueued != null) {
for (SavedQueuedMedia item : savedMediaQueued) {
Bitmap preview = getImageThumbnail(getContentResolver(), item.uri);
Bitmap preview = MediaUtils.getImageThumbnail(getContentResolver(), item.uri, THUMBNAIL_SIZE);
addMediaToQueue(item.type, preview, item.uri, item.mediaSize, item.readyStage);
}
} else if (intent != null && savedInstanceState == null) {
@ -453,25 +431,27 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
if (type != null) {
if (type.startsWith("image/")) {
List<Uri> uriList = new ArrayList<>();
switch (intent.getAction()) {
case Intent.ACTION_SEND: {
Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (uri != null) {
uriList.add(uri);
if (intent.getAction() != null) {
switch (intent.getAction()) {
case Intent.ACTION_SEND: {
Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (uri != null) {
uriList.add(uri);
}
break;
}
break;
}
case Intent.ACTION_SEND_MULTIPLE: {
ArrayList<Uri> list = intent.getParcelableArrayListExtra(
Intent.EXTRA_STREAM);
if (list != null) {
for (Uri uri : list) {
if (uri != null) {
uriList.add(uri);
case Intent.ACTION_SEND_MULTIPLE: {
ArrayList<Uri> list = intent.getParcelableArrayListExtra(
Intent.EXTRA_STREAM);
if (list != null) {
for (Uri uri : list) {
if (uri != null) {
uriList.add(uri);
}
}
}
break;
}
break;
}
}
for (Uri uri : uriList) {
@ -506,7 +486,6 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
}
outState.putParcelableArrayList("savedMediaQueued", savedMediaQueued);
outState.putBoolean("showMarkSensitive", showMarkSensitive);
outState.putString("statusVisibility", statusVisibility);
outState.putBoolean("statusMarkSensitive", statusMarkSensitive);
outState.putBoolean("statusHideText", statusHideText);
if (currentInputContentInfo != null) {
@ -701,6 +680,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
return c;
}
@SuppressLint("StaticFieldLeak")
private boolean saveTheToot(String s, @Nullable String contentWarning) {
if (TextUtils.isEmpty(s)) {
return false;
@ -715,14 +695,11 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
}.getType());
}
final TootEntity toot = new TootEntity();
toot.setText(s);
toot.setContentWarning(contentWarning);
String mediaUrlsSerialized = null;
if (!ListUtils.isEmpty(mediaQueued)) {
List<String> savedList = saveMedia(existingUris);
if (!ListUtils.isEmpty(savedList)) {
String json = new Gson().toJson(savedList);
toot.setUrls(json);
mediaUrlsSerialized = new Gson().toJson(savedList);
if (!ListUtils.isEmpty(existingUris)) {
deleteMedia(setDifference(existingUris, savedList));
}
@ -734,16 +711,15 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
* can be deleted. */
deleteMedia(existingUris);
}
final TootEntity toot = new TootEntity(savedTootUid, s, mediaUrlsSerialized, contentWarning,
inReplyToId,
getIntent().getStringExtra(REPLYING_STATUS_CONTENT_EXTRA),
getIntent().getStringExtra(REPLYING_STATUS_AUTHOR_USERNAME_EXTRA), statusVisibility);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
if (savedTootUid != 0) {
toot.setUid(savedTootUid);
tootDao.updateToot(toot);
} else {
tootDao.insert(toot);
}
tootDao.insertOrReplace(toot);
return null;
}
}.execute();
@ -759,10 +735,10 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
}
}
private void setStatusVisibility(String visibility) {
private void setStatusVisibility(Status.Visibility visibility) {
statusVisibility = visibility;
switch (visibility) {
case "public": {
case PUBLIC: {
floatingBtn.setText(R.string.action_send_public);
floatingBtn.setCompoundDrawables(null, null, null, null);
Drawable globe = AppCompatResources.getDrawable(this, R.drawable.ic_public_24dp);
@ -771,7 +747,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
}
break;
}
case "private": {
case PRIVATE: {
addLockToSendButton();
Drawable lock = AppCompatResources.getDrawable(this,
R.drawable.ic_lock_outline_24dp);
@ -780,7 +756,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
}
break;
}
case "direct": {
case DIRECT: {
addLockToSendButton();
Drawable envelope = AppCompatResources.getDrawable(this, R.drawable.ic_email_24dp);
if (envelope != null) {
@ -788,7 +764,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
}
break;
}
case "unlisted":
case UNLISTED:
default: {
floatingBtn.setText(R.string.action_send);
floatingBtn.setCompoundDrawables(null, null, null, null);
@ -808,7 +784,8 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
fragment.show(getSupportFragmentManager(), null);
}
public void onVisibilityChanged(String visibility) {
@Override
public void onVisibilityChanged(Status.Visibility visibility) {
setStatusVisibility(visibility);
}
@ -848,18 +825,17 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
@Override
protected void onStop() {
super.onStop();
if (inReplyToId != null) {
/* Don't save the visibility setting for replies because they adopt the visibility of
* the status they reply to and that behaviour needs to be kept separate. */
return;
// Don't save the visibility setting for replies because they adopt the visibility of
// the status they reply to and that behaviour needs to be kept separate.
if (inReplyToId == null) {
getPrivatePreferences().edit()
.putInt(REMEMBERED_VISIBILITY_PREF, statusVisibility.getNum())
.apply();
}
getPrivatePreferences().edit()
.putString("rememberedVisibility", statusVisibility)
.apply();
}
private void setEditTextMimeTypes() {
final String[] mimeTypes = new String[] {"image/*"};
final String[] mimeTypes = new String[]{"image/*"};
textEditor.setMimeTypes(mimeTypes, new InputConnectionCompat.OnCommitContentListener() {
@Override
public boolean onCommitContent(InputContentInfoCompat inputContentInfo,
@ -932,7 +908,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
return true;
}
private void sendStatus(String content, String visibility, boolean sensitive,
private void sendStatus(String content, Status.Visibility visibility, boolean sensitive,
String spoilerText) {
ArrayList<String> mediaIds = new ArrayList<>();
@ -946,25 +922,23 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
if (response.isSuccessful()) {
onSendSuccess();
} else {
onSendFailure();
onSendFailure(response);
}
}
@Override
public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) {
onSendFailure();
onSendFailure(null);
}
};
mastodonApi.createStatus(content, inReplyToId, spoilerText, visibility, sensitive, mediaIds)
.enqueue(callback);
mastodonApi.createStatus(content, inReplyToId, spoilerText, visibility.serverString(),
sensitive, mediaIds).enqueue(callback);
}
private void onSendSuccess() {
// If the status was loaded from a draft, delete the draft and associated media files.
if (savedTootUid != 0) {
TootEntity status = new TootEntity();
status.setUid(savedTootUid);
tootDao.delete(status);
tootDao.delete(savedTootUid);
for (QueuedMedia item : mediaQueued) {
try {
if (getContentResolver().delete(item.uri, null, null) == 0) {
@ -983,16 +957,32 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
finish();
}
private void onSendFailure() {
textEditor.setError(getString(R.string.error_generic));
private void onSendFailure(@Nullable Response<Status> response) {
setStateToNotReadying();
if (response != null && inReplyToId != null && response.code() == 404) {
new AlertDialog.Builder(this)
.setMessage(R.string.dialog_reply_not_found)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
inReplyToId = null;
replyContentTextView.setVisibility(View.GONE);
replyTextView.setVisibility(View.GONE);
}
})
.setNegativeButton(android.R.string.cancel, null)
.show();
} else {
textEditor.setError(getString(R.string.error_generic));
}
}
private void readyStatus(final String visibility, final boolean sensitive) {
private void readyStatus(final Status.Visibility visibility, final boolean sensitive) {
finishingUploadDialog = ProgressDialog.show(
this, getString(R.string.dialog_title_finishing_media_upload),
getString(R.string.dialog_message_uploading_media), true, true);
final AsyncTask<Void, Void, Boolean> waitForMediaTask =
@SuppressLint("StaticFieldLeak") final AsyncTask<Void, Void, Boolean> waitForMediaTask =
new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... params) {
@ -1035,7 +1025,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
waitForMediaTask.execute();
}
private void onReadySuccess(String visibility, boolean sensitive) {
private void onReadySuccess(Status.Visibility visibility, boolean sensitive) {
/* Validate the status meets the character limit. This has to be delayed until after all
* uploads finish because their links are added when the upload succeeds and that affects
* whether the limit is met or not. */
@ -1056,7 +1046,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
}
}
private void onReadyFailure(final String visibility, final boolean sensitive) {
private void onReadyFailure(final Status.Visibility visibility, final boolean sensitive) {
doErrorDialog(R.string.error_media_upload_sending, R.string.action_retry,
new View.OnClickListener() {
@Override
@ -1439,44 +1429,6 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
}
}
@Nullable
private static Bitmap getImageThumbnail(ContentResolver contentResolver, Uri uri) {
InputStream stream;
try {
stream = contentResolver.openInputStream(uri);
} catch (FileNotFoundException e) {
return null;
}
Bitmap source = BitmapFactory.decodeStream(stream);
if (source == null) {
IOUtils.closeQuietly(stream);
return null;
}
Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, THUMBNAIL_SIZE, THUMBNAIL_SIZE);
source.recycle();
try {
if (stream != null) {
stream.close();
}
} catch (IOException e) {
bitmap.recycle();
return null;
}
return bitmap;
}
@Nullable
private static Bitmap getVideoThumbnail(Context context, Uri uri) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(context, uri);
Bitmap source = retriever.getFrameAtTime();
if (source == null) {
return null;
}
Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, THUMBNAIL_SIZE, THUMBNAIL_SIZE);
source.recycle();
return bitmap;
}
private void pickMedia(Uri uri, long mediaSize) {
ContentResolver contentResolver = getContentResolver();
@ -1498,7 +1450,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
displayTransientError(R.string.error_media_upload_image_or_video);
return;
}
Bitmap bitmap = getVideoThumbnail(this, uri);
Bitmap bitmap = MediaUtils.getVideoThumbnail(this, uri, THUMBNAIL_SIZE);
if (bitmap != null) {
addMediaToQueue(QueuedMedia.Type.VIDEO, bitmap, uri, mediaSize, null);
} else {
@ -1507,7 +1459,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
break;
}
case "image": {
Bitmap bitmap = getImageThumbnail(contentResolver, uri);
Bitmap bitmap = MediaUtils.getImageThumbnail(contentResolver, uri, THUMBNAIL_SIZE);
if (bitmap != null) {
addMediaToQueue(QueuedMedia.Type.IMAGE, bitmap, uri, mediaSize, null);
} else {
@ -1562,10 +1514,8 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
return super.onOptionsItemSelected(item);
}
/**
* Does a synchronous search request for accounts fulfilling the given partial mention text.
*/
private ArrayList<Account> autocompleteMention(String mention) {
@Override
public List<Account> searchAccounts(String mention) {
ArrayList<Account> resultList = new ArrayList<>();
try {
List<Account> accountList = mastodonApi.searchAccounts(mention, false, 40)
@ -1580,7 +1530,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
return resultList;
}
private static class QueuedMedia {
private static final class QueuedMedia {
Type type;
ProgressImageView preview;
Uri uri;
@ -1657,98 +1607,42 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
}
}
private class MentionAutoCompleteAdapter extends ArrayAdapter<Account> implements Filterable {
private ArrayList<Account> resultList;
@LayoutRes
private int layoutId;
MentionAutoCompleteAdapter(Context context, @LayoutRes int resource) {
super(context, resource);
layoutId = resource;
resultList = new ArrayList<>();
/**
* Function to decide which visibility should be used for posting a status
*
* @return {@code PRIVATE} if account is locked, {@code PUBLIC} if both start and reply
* visibilities are unknown or minimal known visibility of two of them.
*/
private static Status.Visibility pickVisibility(final Status.Visibility startVisibility,
final Status.Visibility replyVisibility,
boolean isAccountLocked) {
// If the currently logged in account is locked, its posts should default to private.
// This should override even the reply settings.
if (isAccountLocked) {
return Status.Visibility.PRIVATE;
}
@Override
public int getCount() {
return resultList.size();
if (startVisibility == Status.Visibility.UNKNOWN &&
replyVisibility == Status.Visibility.UNKNOWN) {
return Status.Visibility.PUBLIC;
}
@Override
public Account getItem(int index) {
return resultList.get(index);
if (replyVisibility == Status.Visibility.UNKNOWN) {
return startVisibility;
}
@Override
@NonNull
public Filter getFilter() {
return new Filter() {
@Override
public CharSequence convertResultToString(Object resultValue) {
return ((Account) resultValue).username;
}
// This method is invoked in a worker thread.
@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults filterResults = new FilterResults();
if (constraint != null) {
ArrayList<Account> accounts = autocompleteMention(constraint.toString());
filterResults.values = accounts;
filterResults.count = accounts.size();
}
return filterResults;
}
@SuppressWarnings("unchecked")
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
if (results != null && results.count > 0) {
resultList.clear();
ArrayList<Account> newResults = (ArrayList<Account>) results.values;
resultList.addAll(newResults);
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
};
if (startVisibility == Status.Visibility.UNKNOWN) {
return replyVisibility;
}
@Override
@NonNull
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view = convertView;
Context context = getContext();
if (convertView == null) {
LayoutInflater layoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = layoutInflater.inflate(layoutId, null);
}
Account account = getItem(position);
if (account != null) {
TextView username = view.findViewById(R.id.username);
TextView displayName = view.findViewById(R.id.display_name);
ImageView avatar = view.findViewById(R.id.avatar);
String format = getContext().getString(R.string.status_username_format);
String formattedUsername = String.format(format, account.username);
username.setText(formattedUsername);
displayName.setText(account.getDisplayName());
if (!account.avatar.isEmpty()) {
Picasso.with(context)
.load(account.avatar)
.placeholder(R.drawable.avatar_default)
.transform(new RoundedTransformation(7, 0))
.into(avatar);
}
}
return view;
if (startVisibility.getNum() > replyVisibility.getNum()) {
return startVisibility;
} else {
return replyVisibility;
}
}
@SuppressWarnings("WeakerAccess")
public static final class IntentBuilder {
@Nullable
private Integer savedTootUid;
@ -1761,11 +1655,11 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
@Nullable
private String inReplyToId;
@Nullable
private String replyVisibility;
private Status.Visibility replyVisibility;
@Nullable
private String contentWarning;
@Nullable
private Account replyingStatusAuthor;
private String replyingStatusAuthor;
@Nullable
private String replyingStatusContent;
@ -1794,7 +1688,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
return this;
}
public IntentBuilder replyVisibility(String replyVisibility) {
public IntentBuilder replyVisibility(Status.Visibility replyVisibility) {
this.replyVisibility = replyVisibility;
return this;
}
@ -1804,8 +1698,8 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
return this;
}
public IntentBuilder repyingStatusAuthor(Account author) {
this.replyingStatusAuthor = author;
public IntentBuilder repyingStatusAuthor(String username) {
this.replyingStatusAuthor = username;
return this;
}
@ -1834,7 +1728,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
intent.putExtra(IN_REPLY_TO_ID_EXTRA, inReplyToId);
}
if (replyVisibility != null) {
intent.putExtra(REPLY_VISIBILITY_EXTRA, replyVisibility);
intent.putExtra(REPLY_VISIBILITY_EXTRA, replyVisibility.getNum());
}
if (contentWarning != null) {
intent.putExtra(CONTENT_WARNING_EXTRA, contentWarning);
@ -1843,8 +1737,7 @@ public final class ComposeActivity extends BaseActivity implements ComposeOption
intent.putExtra(REPLYING_STATUS_CONTENT_EXTRA, replyingStatusContent);
}
if (replyingStatusAuthor != null) {
intent.putExtra(REPLYING_STATUS_AUTHOR_USERNAME_EXTRA,
replyingStatusAuthor.localUsername);
intent.putExtra(REPLYING_STATUS_AUTHOR_USERNAME_EXTRA, replyingStatusAuthor);
}
return intent;
}

View file

@ -20,6 +20,7 @@ import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
@ -37,6 +38,7 @@ import com.keylesspalace.tusky.db.TootDao;
import com.keylesspalace.tusky.db.TootEntity;
import com.keylesspalace.tusky.util.ThemeUtils;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
@ -50,6 +52,9 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.
private SavedTootAdapter adapter;
private TextView noContent;
private List<TootEntity> toots = new ArrayList<>();
@Nullable private AsyncTask<?, ?, ?> asyncTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -82,9 +87,13 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.
@Override
protected void onResume() {
super.onResume();
fetchToots();
}
// req
getAllToot();
@Override
protected void onPause() {
super.onPause();
if (asyncTask != null) asyncTask.cancel(true);
}
@Override
@ -98,24 +107,9 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.
return super.onOptionsItemSelected(item);
}
private void getAllToot() {
new AsyncTask<Void, Void, List<TootEntity>>() {
@Override
protected List<TootEntity> doInBackground(Void... params) {
return tootDao.loadAll();
}
@Override
protected void onPostExecute(List<TootEntity> tootEntities) {
super.onPostExecute(tootEntities);
// set ui
setNoContent(tootEntities.size());
if (adapter != null) {
adapter.setItems(tootEntities);
adapter.notifyDataSetChanged();
}
}
}.execute();
private void fetchToots() {
asyncTask = new FetchPojosTask(this)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void setNoContent(int size) {
@ -140,11 +134,12 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.
}
}
// update DB
tootDao.delete(item);
tootDao.delete(item.getUid());
toots.remove(position);
// update adapter
if (adapter != null) {
adapter.removeItem(position);
setNoContent(adapter.getItemCount());
setNoContent(toots.size());
}
}
@ -155,7 +150,41 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.
.savedTootText(item.getText())
.contentWarning(item.getContentWarning())
.savedJsonUrls(item.getUrls())
.inReplyToId(item.getInReplyToId())
.repyingStatusAuthor(item.getInReplyToUsername())
.replyingStatusContent(item.getInReplyToText())
.replyVisibility(item.getVisibility())
.build(this);
startActivity(intent);
}
static final class FetchPojosTask extends AsyncTask<Void, Void, List<TootEntity>> {
private final WeakReference<SavedTootActivity> activityRef;
FetchPojosTask(SavedTootActivity activity) {
this.activityRef = new WeakReference<>(activity);
}
@Override
protected List<TootEntity> doInBackground(Void... voids) {
return tootDao.loadAll();
}
@Override
protected void onPostExecute(List<TootEntity> pojos) {
super.onPostExecute(pojos);
SavedTootActivity activity = activityRef.get();
if (activity == null) return;
activity.toots.addAll(pojos);
// set ui
activity.setNoContent(pojos.size());
List<TootEntity> toots = new ArrayList<>(pojos.size());
toots.addAll(pojos);
activity.adapter.setItems(toots);
activity.adapter.notifyDataSetChanged();
}
}
}

View file

@ -56,6 +56,7 @@ public class TuskyApplication extends Application {
db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "tuskyDB")
.allowMainThreadQueries()
.addMigrations(AppDatabase.MIGRATION_2_3)
.addMigrations(AppDatabase.MIGRATION_3_4)
.build();
JobManager.create(this).addJobCreator(new NotificationPullJobCreator(this));

View file

@ -0,0 +1,143 @@
/* 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.adapter;
import android.content.Context;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.ImageView;
import android.widget.TextView;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.view.RoundedTransformation;
import com.squareup.picasso.Picasso;
import java.util.ArrayList;
import java.util.List;
/**
* Created by charlag on 12/11/17.
*/
public class MentionAutoCompleteAdapter extends ArrayAdapter<Account>
implements Filterable {
private ArrayList<Account> resultList;
@LayoutRes
private int layoutId;
private final AccountSearchProvider accountSearchProvider;
public MentionAutoCompleteAdapter(Context context, @LayoutRes int resource,
AccountSearchProvider accountSearchProvider) {
super(context, resource);
layoutId = resource;
resultList = new ArrayList<>();
this.accountSearchProvider = accountSearchProvider;
}
@Override
public int getCount() {
return resultList.size();
}
@Override
public Account getItem(int index) {
return resultList.get(index);
}
@Override
@NonNull
public Filter getFilter() {
return new Filter() {
@Override
public CharSequence convertResultToString(Object resultValue) {
return ((Account) resultValue).username;
}
// This method is invoked in a worker thread.
@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults filterResults = new FilterResults();
if (constraint != null) {
List<Account> accounts =
accountSearchProvider.searchAccounts(constraint.toString());
filterResults.values = accounts;
filterResults.count = accounts.size();
}
return filterResults;
}
@SuppressWarnings("unchecked")
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
if (results != null && results.count > 0) {
resultList.clear();
ArrayList<Account> newResults = (ArrayList<Account>) results.values;
resultList.addAll(newResults);
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
};
}
@Override
@NonNull
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view = convertView;
Context context = getContext();
if (convertView == null) {
LayoutInflater layoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//noinspection ConstantConditions
view = layoutInflater.inflate(layoutId, parent, false);
}
Account account = getItem(position);
if (account != null) {
TextView username = view.findViewById(R.id.username);
TextView displayName = view.findViewById(R.id.display_name);
ImageView avatar = view.findViewById(R.id.avatar);
String format = getContext().getString(R.string.status_username_format);
String formattedUsername = String.format(format, account.username);
username.setText(formattedUsername);
displayName.setText(account.getDisplayName());
if (!account.avatar.isEmpty()) {
Picasso.with(context)
.load(account.avatar)
.placeholder(R.drawable.avatar_default)
.transform(new RoundedTransformation(7, 0))
.into(avatar);
}
}
return view;
}
public interface AccountSearchProvider {
List<Account> searchAccounts(String mention);
}
}

View file

@ -1,25 +1,39 @@
/* 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.db;
import android.arch.persistence.db.SupportSQLiteDatabase;
import android.arch.persistence.room.Database;
import android.arch.persistence.room.RoomDatabase;
import android.arch.persistence.room.migration.Migration;
import android.support.annotation.NonNull;
/**
* DB version & declare DAO
*/
@Database(entities = {TootEntity.class}, version = 3, exportSchema = false)
@Database(entities = {TootEntity.class}, version = 4, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
public abstract TootDao tootDao();
public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
//this migration is necessary because of a change in the room library
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE TootEntity2 (uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, text TEXT, urls TEXT, contentWarning TEXT);");
database.execSQL("INSERT INTO TootEntity2 SELECT * FROM TootEntity;");
database.execSQL("DROP TABLE TootEntity;");
database.execSQL("ALTER TABLE TootEntity2 RENAME TO TootEntity;");
@ -27,4 +41,13 @@ public abstract class AppDatabase extends RoomDatabase {
}
};
public static final Migration MIGRATION_3_4 = new Migration(3, 4) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE TootEntity ADD COLUMN inReplyToId TEXT");
database.execSQL("ALTER TABLE TootEntity ADD COLUMN inReplyToText TEXT");
database.execSQL("ALTER TABLE TootEntity ADD COLUMN inReplyToUsername TEXT");
database.execSQL("ALTER TABLE TootEntity ADD COLUMN visibility INTEGER");
}
};
}

View file

@ -1,33 +1,43 @@
/* 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.db;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Delete;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Transaction;
import android.arch.persistence.room.Update;
import java.util.List;
/**
* Created by cto3543 on 28/06/2017.
* crud interface on this Toot DB
*
* DAO to fetch and update toots in the DB.
*/
@Dao
public interface TootDao {
// c
@Insert
long insert(TootEntity users);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertOrReplace(TootEntity users);
// r
@Query("SELECT * FROM TootEntity")
@Query("SELECT * FROM TootEntity ORDER BY uid DESC")
List<TootEntity> loadAll();
// u
@Update
void updateToot(TootEntity toot);
// d
@Delete
int delete(TootEntity user);
@Query("DELETE FROM TootEntity WHERE uid = :uid")
int delete(int uid);
}

View file

@ -1,57 +1,119 @@
/* 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.db;
import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.PrimaryKey;
import android.arch.persistence.room.TypeConverter;
import android.arch.persistence.room.TypeConverters;
import android.support.annotation.Nullable;
import com.keylesspalace.tusky.entity.Status;
/**
* toot model
* Toot model.
*/
@Entity
@TypeConverters(TootEntity.Converters.class)
public class TootEntity {
@PrimaryKey(autoGenerate = true)
private int uid;
private final int uid;
@ColumnInfo(name = "text")
private String text;
private final String text;
@ColumnInfo(name = "urls")
private String urls;
private final String urls;
@ColumnInfo(name = "contentWarning")
private String contentWarning;
private final String contentWarning;
// getter setter
public String getText() {
return text;
@ColumnInfo(name = "inReplyToId")
private final String inReplyToId;
@Nullable
@ColumnInfo(name = "inReplyToText")
private final String inReplyToText;
@Nullable
@ColumnInfo(name = "inReplyToUsername")
private final String inReplyToUsername;
@ColumnInfo(name = "visibility")
private final Status.Visibility visibility;
public TootEntity(int uid, String text, String urls, String contentWarning, String inReplyToId,
@Nullable String inReplyToText, @Nullable String inReplyToUsername,
Status.Visibility visibility) {
this.uid = uid;
this.text = text;
this.urls = urls;
this.contentWarning = contentWarning;
this.inReplyToId = inReplyToId;
this.inReplyToText = inReplyToText;
this.inReplyToUsername = inReplyToUsername;
this.visibility = visibility;
}
public void setText(String text) {
this.text = text;
public String getText() {
return text;
}
public String getContentWarning() {
return contentWarning;
}
public void setContentWarning(String contentWarning) {
this.contentWarning = contentWarning;
}
public int getUid() {
return uid;
}
public void setUid(int uid) {
this.uid = uid;
}
public String getUrls() {
return urls;
}
public void setUrls(String urls) {
this.urls = urls;
public String getInReplyToId() {
return inReplyToId;
}
@Nullable
public String getInReplyToText() {
return inReplyToText;
}
@Nullable
public String getInReplyToUsername() {
return inReplyToUsername;
}
public Status.Visibility getVisibility() {
return visibility;
}
public static final class Converters {
@TypeConverter
public Status.Visibility visibilityFromInt(int number) {
return Status.Visibility.byNum(number);
}
@TypeConverter
public int intToVisibility(Status.Visibility visibility) {
return visibility.getNum();
}
}
}

View file

@ -50,15 +50,45 @@ public class Status {
}
public enum Visibility {
UNKNOWN,
UNKNOWN(0),
@SerializedName("public")
PUBLIC,
PUBLIC(1),
@SerializedName("unlisted")
UNLISTED,
UNLISTED(2),
@SerializedName("private")
PRIVATE,
PRIVATE(3),
@SerializedName("direct")
DIRECT,
DIRECT(4);
private final int num;
Visibility(int num) {
this.num = num;
}
public int getNum() {
return num;
}
public static Visibility byNum(int num) {
switch (num) {
case 4: return DIRECT;
case 3: return PRIVATE;
case 2: return UNLISTED;
case 1: return PUBLIC;
case 0: default: return UNKNOWN;
}
}
public String serverString() {
switch (this) {
case PUBLIC: return "public";
case UNLISTED: return "unlisted";
case PRIVATE: return "private";
case DIRECT: return "direct";
case UNKNOWN: default: return "unknown";
}
}
}
public String id;
@ -162,7 +192,7 @@ public class Status {
}
}
public static class Mention {
public static final class Mention {
public String id;
public String url;
@ -179,6 +209,7 @@ public class Status {
public String website;
}
@SuppressWarnings("unused")
public static class Emoji {
private String shortcode;
private String url;

View file

@ -34,11 +34,12 @@ import android.widget.RadioButton;
import android.widget.RadioGroup;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.util.ThemeUtils;
public class ComposeOptionsFragment extends BottomSheetDialogFragment {
public interface Listener {
void onVisibilityChanged(String visibility);
void onVisibilityChanged(Status.Visibility visibility);
void onContentWarningChanged(boolean hideText);
}
@ -46,10 +47,10 @@ public class ComposeOptionsFragment extends BottomSheetDialogFragment {
private CheckBox hideText;
private Listener listener;
public static ComposeOptionsFragment newInstance(String visibility, boolean hideText) {
public static ComposeOptionsFragment newInstance(Status.Visibility visibility, boolean hideText) {
Bundle arguments = new Bundle();
ComposeOptionsFragment fragment = new ComposeOptionsFragment();
arguments.putString("visibility", visibility);
arguments.putInt("visibilityNum", visibility.getNum());
arguments.putBoolean("hideText", hideText);
fragment.setArguments(arguments);
return fragment;
@ -68,18 +69,18 @@ public class ComposeOptionsFragment extends BottomSheetDialogFragment {
View rootView = inflater.inflate(R.layout.fragment_compose_options, container, false);
Bundle arguments = getArguments();
String statusVisibility = arguments.getString("visibility");
Status.Visibility visibility = Status.Visibility.byNum(
arguments.getInt("visibilityNum", 0)
);
boolean statusHideText = arguments.getBoolean("hideText");
radio = rootView.findViewById(R.id.radio_visibility);
int radioCheckedId = R.id.radio_public;
if (statusVisibility != null) {
switch (statusVisibility) {
case "public": radioCheckedId = R.id.radio_public; break;
case "private": radioCheckedId = R.id.radio_private; break;
case "unlisted": radioCheckedId = R.id.radio_unlisted; break;
case "direct": radioCheckedId = R.id.radio_direct; break;
}
switch (visibility) {
case PUBLIC: radioCheckedId = R.id.radio_public; break;
case PRIVATE: radioCheckedId = R.id.radio_private; break;
case UNLISTED: radioCheckedId = R.id.radio_unlisted; break;
case DIRECT: radioCheckedId = R.id.radio_direct; break;
}
radio.check(radioCheckedId);
@ -104,23 +105,23 @@ public class ComposeOptionsFragment extends BottomSheetDialogFragment {
radio.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
String visibility;
Status.Visibility visibility;
switch (checkedId) {
default:
case R.id.radio_public: {
visibility = "public";
visibility = Status.Visibility.PUBLIC;
break;
}
case R.id.radio_unlisted: {
visibility = "unlisted";
visibility = Status.Visibility.UNLISTED;
break;
}
case R.id.radio_private: {
visibility = "private";
visibility = Status.Visibility.PRIVATE;
break;
}
case R.id.radio_direct: {
visibility = "direct";
visibility = Status.Visibility.DIRECT;
break;
}
}

View file

@ -93,7 +93,7 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
protected void reply(Status status) {
String inReplyToId = status.getActionableId();
Status actionableStatus = status.getActionableStatus();
String replyVisibility = actionableStatus.getVisibility().toString().toLowerCase();
Status.Visibility replyVisibility = actionableStatus.getVisibility();
String contentWarning = actionableStatus.spoilerText;
Status.Mention[] mentions = actionableStatus.mentions;
List<String> mentionedUsernames = new ArrayList<>();
@ -107,7 +107,7 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
.replyVisibility(replyVisibility)
.contentWarning(contentWarning)
.mentionedUsernames(mentionedUsernames)
.repyingStatusAuthor(actionableStatus.account)
.repyingStatusAuthor(actionableStatus.account.localUsername)
.replyingStatusContent(actionableStatus.content.toString())
.build(getContext());
startActivityForResult(intent, COMPOSE_RESULT);

View file

@ -320,9 +320,9 @@ public class ViewThreadFragment extends SFragment implements
call.enqueue(new Callback<StatusContext>() {
@Override
public void onResponse(@NonNull Call<StatusContext> call, @NonNull Response<StatusContext> response) {
if (response.isSuccessful()) {
StatusContext context = response.body();
if (response.isSuccessful() && context != null) {
swipeRefreshLayout.setRefreshing(false);
StatusContext context = response.body();
setContext(context.ancestors, context.descendants);
} else {
onThreadRequestFailure(id);

View file

@ -19,11 +19,15 @@ import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadataRetriever;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.os.Environment;
import android.provider.OpenableColumns;
import android.support.annotation.Nullable;
import android.support.annotation.Px;
import android.support.v4.content.FileProvider;
import com.squareup.picasso.Picasso;
@ -31,6 +35,7 @@ import com.squareup.picasso.Target;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@ -83,60 +88,43 @@ public class MediaUtils {
return mediaSize;
}
/** Download an image with picasso asynchronously and call the given listener when completed. */
public static Target picassoImageTarget(final Context context, final MediaListener mediaListener) {
final String imageName = "temp";
return new Target() {
@Override
public void onBitmapLoaded(final Bitmap bitmap, Picasso.LoadedFrom from) {
new Thread(new Runnable() {
@Override
public void run() {
FileOutputStream fos = null;
Uri uriForFile;
try {
// we download only a "temp" file
File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File tempFile = File.createTempFile(
imageName,
".jpg",
storageDir
);
fos = new FileOutputStream(tempFile);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
uriForFile = FileProvider.getUriForFile(context,
"com.keylesspalace.tusky.fileprovider",
tempFile);
// giving to the activity the URI callback
mediaListener.onCallback(uriForFile);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
@Nullable
public static Bitmap getImageThumbnail(ContentResolver contentResolver, Uri uri,
@Px int thumbnailSize) {
InputStream stream;
try {
stream = contentResolver.openInputStream(uri);
} catch (FileNotFoundException e) {
return null;
}
Bitmap source = BitmapFactory.decodeStream(stream);
if (source == null) {
IOUtils.closeQuietly(stream);
return null;
}
Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, thumbnailSize, thumbnailSize);
source.recycle();
try {
if (stream != null) {
stream.close();
}
@Override
public void onBitmapFailed(Drawable errorDrawable) {
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
}
};
} catch (IOException e) {
bitmap.recycle();
return null;
}
return bitmap;
}
public interface MediaListener {
void onCallback(Uri headerInfo);
@Nullable
public static Bitmap getVideoThumbnail(Context context, Uri uri, @Px int thumbnailSize) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(context, uri);
Bitmap source = retriever.getFrameAtTime();
if (source == null) {
return null;
}
Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, thumbnailSize, thumbnailSize);
source.recycle();
return bitmap;
}
}

View file

@ -138,6 +138,7 @@
<string name="dialog_download_image">Скачать</string>
<string name="dialog_message_follow_request">Статус запроса на подписку: ожидается ответ</string>
<string name="dialog_unfollow_warning">Отписаться от этого аккаунта?</string>
<string name="dialog_reply_not_found">Не удалось опубликовать статус. Статус, на который вы отвечаете, может быть недоступен. Убрать информацию об ответе?</string>
<string name="visibility_public">Публичный: Показать в публичных лентах</string>
<string name="visibility_unlisted">Скрытый: Не показывать в лентах</string>

View file

@ -146,6 +146,7 @@
<string name="dialog_download_image">Download</string>
<string name="dialog_message_follow_request">Follow request pending: awaiting their response</string>
<string name="dialog_unfollow_warning">Unfollow this account?</string>
<string name="dialog_reply_not_found">Couldn\'t post this status. The status you\'re replying to might not be available. Remove reply info?</string>
<string name="visibility_public">Public: Post to public timelines</string>
<string name="visibility_unlisted">Unlisted: Do not show in public timelines</string>