diff --git a/app/build.gradle b/app/build.gradle
index 11bd95a6a..667380c42 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -87,7 +87,7 @@ dependencies {
testImplementation "org.robolectric:robolectric:3.8"
testImplementation "org.mockito:mockito-inline:2.17.0"
- androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
+ androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.1', {
exclude group: 'com.android.support', module: 'support-annotations'
})
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5232166b7..fafd824dd 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -111,6 +111,7 @@
+
mediaQueued = new ArrayList<>();
private CountUpDownLatch waitForMediaLatch;
- private boolean showMarkSensitive;
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 statusAlreadyInFlight; // to prevent duplicate sends by mashing the send button
private InputContentInfoCompat currentInputContentInfo;
private int currentFlags;
private Uri photoUploadUri;
private int savedTootUid = 0;
+ private SaveTootHelper saveTootHelper;
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_compose);
- replyTextView = findViewById(R.id.reply_tv);
- replyContentTextView = findViewById(R.id.reply_content_tv);
- textEditor = findViewById(R.id.compose_edit_field);
+ replyTextView = findViewById(R.id.composeReplyView);
+ replyContentTextView = findViewById(R.id.composeReplyContentView);
+ textEditor = findViewById(R.id.composeEditField);
mediaPreviewBar = findViewById(R.id.compose_media_preview_bar);
- contentWarningBar = findViewById(R.id.compose_content_warning_bar);
- contentWarningEditor = findViewById(R.id.field_content_warning);
- charactersLeft = findViewById(R.id.characters_left);
- floatingBtn = findViewById(R.id.floating_btn);
- pickButton = findViewById(R.id.compose_photo_pick);
- visibilityBtn = findViewById(R.id.action_toggle_visibility);
- saveButton = findViewById(R.id.compose_save_draft);
- hideMediaToggle = findViewById(R.id.action_hide_media);
- postProgress = findViewById(R.id.postProgress);
+ contentWarningBar = findViewById(R.id.composeContentWarningBar);
+ contentWarningEditor = findViewById(R.id.composeContentWarningField);
+ charactersLeft = findViewById(R.id.composeCharactersLeftView);
+ tootButton = findViewById(R.id.composeTootButton);
+ pickButton = findViewById(R.id.composeAddMediaButton);
+ visibilityButton = findViewById(R.id.composeToggleVisibilityButton);
+ contentWarningButton = findViewById(R.id.composeContentWarningButton);
+ emojiButton = findViewById(R.id.composeEmojiButton);
+ hideMediaToggle = findViewById(R.id.composeHideMediaButton);
+ emojiView = findViewById(R.id.emojiView);
+
+ saveTootHelper = new SaveTootHelper(TuskyApplication.getDB().tootDao(), this);
// Setup the toolbar.
Toolbar toolbar = findViewById(R.id.toolbar);
@@ -241,17 +264,65 @@ public final class ComposeActivity extends BaseActivity
return;
}
+ composeOptionsView = findViewById(R.id.composeOptionsBottomSheet);
+ composeOptionsView.setListener(this);
+
+ composeOptionsBehavior = BottomSheetBehavior.from(composeOptionsView);
+ composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
+
+ addMediaBehavior = BottomSheetBehavior.from(findViewById(R.id.addMediaBottomSheet));
+
+ emojiBehavior = BottomSheetBehavior.from(emojiView);
+
+ emojiView.setLayoutManager(new GridLayoutManager(this, 3, GridLayoutManager.HORIZONTAL, false));
+
+ mastodonApi.getCustomEmojis().enqueue(new Callback>() {
+ @Override
+ public void onResponse(@NonNull Call> call, @NonNull Response> response) {
+ List emojiList = response.body();
+
+ if (emojiList != null) {
+
+ emojiView.setAdapter(new EmojiAdapter(emojiList, ComposeActivity.this));
+
+ EmojiListEntity emojiListEntity = new EmojiListEntity(activeAccount.getDomain(), emojiList);
+
+ TuskyApplication.getDB().emojiListDao().insertOrReplace(emojiListEntity);
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Call> call, @NonNull Throwable t) {
+ Log.w(TAG, "error loading custom emojis", t);
+ EmojiListEntity emojiListEntity = TuskyApplication.getDB().emojiListDao().loadEmojisForInstance(activeAccount.getDomain());
+
+ if(emojiListEntity != null) {
+ emojiView.setAdapter(new EmojiAdapter(emojiListEntity.getEmojiList(), ComposeActivity.this));
+ }
+ }
+ });
+
// Setup the interface buttons.
- floatingBtn.setOnClickListener(v -> onSendClicked());
- floatingBtn.setOnLongClickListener(v -> saveDraft());
+ tootButton.setOnClickListener(v -> onSendClicked());
pickButton.setOnClickListener(v -> openPickDialog());
- visibilityBtn.setOnClickListener(v -> showComposeOptions());
- saveButton.setOnClickListener(v -> saveDraft());
+ visibilityButton.setOnClickListener(v -> showComposeOptions());
+ contentWarningButton.setOnClickListener(v-> onContentWarningChanged());
+ emojiButton.setOnClickListener(v -> showEmojis());
hideMediaToggle.setOnClickListener(v -> toggleHideMedia());
- //fix a bug with autocomplete and some keyboards
- int newInputType = textEditor.getInputType() & (textEditor.getInputType() ^ InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
- textEditor.setInputType(newInputType);
+ TextView actionPhotoTake = findViewById(R.id.action_photo_take);
+ TextView actionPhotoPick = findViewById(R.id.action_photo_pick);
+
+ int textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary);
+
+ Drawable cameraIcon = new IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).color(textColor).sizeDp(18);
+ TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(actionPhotoTake, cameraIcon, null, null, null);
+
+ Drawable imageIcon = new IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).color(textColor).sizeDp(18);
+ TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(actionPhotoPick, imageIcon, null, null, null);
+
+ actionPhotoTake.setOnClickListener(v -> initiateCameraApp());
+ actionPhotoPick.setOnClickListener(v -> onMediaPick());
/* Initialise all the state, or restore it from a previous run, to determine a "starting"
* state. */
@@ -260,7 +331,6 @@ public final class ComposeActivity extends BaseActivity
String startingContentWarning = null;
ArrayList savedMediaQueued = null;
if (savedInstanceState != null) {
- showMarkSensitive = savedInstanceState.getBoolean("showMarkSensitive");
startingVisibility = Status.Visibility.byNum(
savedInstanceState.getInt("statusVisibility",
Status.Visibility.PUBLIC.getNum())
@@ -278,7 +348,6 @@ public final class ComposeActivity extends BaseActivity
}
photoUploadUri = savedInstanceState.getParcelable("photoUploadUri");
} else {
- showMarkSensitive = false;
statusMarkSensitive = false;
startingHideText = false;
photoUploadUri = null;
@@ -342,11 +411,24 @@ public final class ComposeActivity extends BaseActivity
replyTextView.setVisibility(View.VISIBLE);
String username = intent.getStringExtra(REPLYING_STATUS_AUTHOR_USERNAME_EXTRA);
replyTextView.setText(getString(R.string.replying_to, username));
+ Drawable arrowDownIcon = new IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_down).sizeDp(12);
+
+ ThemeUtils.setDrawableTint(this, arrowDownIcon, android.R.attr.textColorTertiary);
+ TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(replyTextView, null, null, arrowDownIcon, null);
+
replyTextView.setOnClickListener(v -> {
+ TransitionManager.beginDelayedTransition((ViewGroup)replyContentTextView.getParent());
+
if (replyContentTextView.getVisibility() != View.VISIBLE) {
replyContentTextView.setVisibility(View.VISIBLE);
+ Drawable arrowUpIcon = new IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_up).sizeDp(12);
+
+ ThemeUtils.setDrawableTint(this, arrowUpIcon, android.R.attr.textColorTertiary);
+ TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(replyTextView, null, null, arrowUpIcon, null);
} else {
replyContentTextView.setVisibility(View.GONE);
+
+ TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(replyTextView, null, null, arrowDownIcon, null);
}
});
}
@@ -359,12 +441,11 @@ public final class ComposeActivity extends BaseActivity
// After the starting state is finalised, the interface can be set to reflect this state.
setStatusVisibility(startingVisibility);
- postProgress.setVisibility(View.INVISIBLE);
- updateHideMediaToggleColor();
+ updateHideMediaToggle();
updateVisibleCharactersLeft();
// Setup the main text field.
- setEditTextMimeTypes(); // new String[] { "image/gif", "image/webp" }
+ textEditor.setOnCommitContentListener(this);
final int mentionColour = ThemeUtils.getColor(this, R.attr.compose_mention_color);
SpanUtils.highlightSpans(textEditor.getText(), mentionColour);
textEditor.addTextChangedListener(new TextWatcher() {
@@ -421,7 +502,6 @@ public final class ComposeActivity extends BaseActivity
// Initialise the empty media queue state.
waitForMediaLatch = new CountUpDownLatch();
- statusAlreadyInFlight = false;
// These can only be added after everything affected by the media queue is initialized.
if (!ListUtils.isEmpty(loadedDraftMediaUris)) {
@@ -497,7 +577,6 @@ public final class ComposeActivity extends BaseActivity
item.mediaSize, item.readyStage, item.description));
}
outState.putParcelableArrayList("savedMediaQueued", savedMediaQueued);
- outState.putBoolean("showMarkSensitive", showMarkSensitive);
outState.putBoolean("statusMarkSensitive", statusMarkSensitive);
outState.putBoolean("statusHideText", statusHideText);
if (currentInputContentInfo != null) {
@@ -517,274 +596,98 @@ public final class ComposeActivity extends BaseActivity
Snackbar bar = Snackbar.make(findViewById(R.id.activity_compose), getString(descriptionId),
Snackbar.LENGTH_SHORT);
bar.setAction(actionId, listener);
+ //necessary so snackbar is shown over everything
+ ViewCompat.setElevation(bar.getView(), getResources().getDimensionPixelSize(R.dimen.compose_activity_snackbar_elevation));
bar.show();
}
private void displayTransientError(@StringRes int stringId) {
- Snackbar.make(findViewById(R.id.activity_compose), stringId, Snackbar.LENGTH_LONG).show();
+ Snackbar bar = Snackbar.make(findViewById(R.id.activity_compose), stringId, Snackbar.LENGTH_LONG);
+ //necessary so snackbar is shown over everything
+ ViewCompat.setElevation(bar.getView(), getResources().getDimensionPixelSize(R.dimen.compose_activity_snackbar_elevation));
+ bar.show();
}
private void toggleHideMedia() {
statusMarkSensitive = !statusMarkSensitive;
- updateHideMediaToggleColor();
+ updateHideMediaToggle();
}
- private void updateHideMediaToggleColor() {
- @AttrRes int attribute;
- if (statusMarkSensitive) {
- attribute = R.attr.compose_hide_media_button_selected_color;
+ private void updateHideMediaToggle() {
+ TransitionManager.beginDelayedTransition((ViewGroup)hideMediaToggle.getParent());
+
+ @ColorInt int color;
+ if(mediaQueued.size() == 0) {
+ hideMediaToggle.setVisibility(View.GONE);
} else {
- attribute = R.attr.compose_hide_media_button_color;
+ hideMediaToggle.setVisibility(View.VISIBLE);
+ if (statusMarkSensitive) {
+ hideMediaToggle.setImageResource(R.drawable.ic_hide_media_24dp);
+ if (statusHideText) {
+ hideMediaToggle.setClickable(false);
+ color = ContextCompat.getColor(this, R.color.compose_media_visible_button_disabled_blue);
+ } else {
+ hideMediaToggle.setClickable(true);
+ color = ContextCompat.getColor(this, R.color.primary);
+ }
+ } else {
+ hideMediaToggle.setClickable(true);
+ hideMediaToggle.setImageResource(R.drawable.ic_eye_24dp);
+ color = ThemeUtils.getColor(this, android.R.attr.textColorTertiary);
+ }
+ hideMediaToggle.getDrawable().setColorFilter(color, PorterDuff.Mode.SRC_IN);
}
- ThemeUtils.setDrawableTint(this, hideMediaToggle.getDrawable(), attribute);
}
private void disableButtons() {
pickButton.setClickable(false);
- visibilityBtn.setClickable(false);
- saveButton.setClickable(false);
+ visibilityButton.setClickable(false);
+ emojiButton.setClickable(false);
hideMediaToggle.setClickable(false);
- floatingBtn.setEnabled(false);
+ tootButton.setEnabled(false);
}
private void enableButtons() {
pickButton.setClickable(true);
- visibilityBtn.setClickable(true);
- saveButton.setClickable(true);
+ visibilityButton.setClickable(true);
+ emojiButton.setClickable(true);
hideMediaToggle.setClickable(true);
- floatingBtn.setEnabled(true);
- }
-
- private boolean saveDraft() {
- String contentWarning = null;
- if (statusHideText) {
- contentWarning = contentWarningEditor.getText().toString();
- }
- Editable textToSave = textEditor.getEditableText();
- /* Discard any upload URLs embedded in the text because they'll be re-uploaded when
- * the draft is loaded and replaced with new URLs. */
- if (mediaQueued != null) {
- for (QueuedMedia item : mediaQueued) {
- textToSave = removeUrlFromEditable(textToSave, item.uploadUrl);
- }
- }
- boolean didSaveSuccessfully = saveTheToot(textToSave.toString(), contentWarning);
- if (didSaveSuccessfully) {
- Toast.makeText(ComposeActivity.this, R.string.action_save_one_toot, Toast.LENGTH_SHORT)
- .show();
- }
- return didSaveSuccessfully;
- }
-
- private static boolean copyToFile(ContentResolver contentResolver, Uri uri, File file) {
- InputStream from;
- FileOutputStream to;
- try {
- from = contentResolver.openInputStream(uri);
- to = new FileOutputStream(file);
- } catch (FileNotFoundException e) {
- return false;
- }
- if (from == null) {
- return false;
- }
- byte[] chunk = new byte[16384];
- try {
- while (true) {
- int bytes = from.read(chunk, 0, chunk.length);
- if (bytes < 0) {
- break;
- }
- to.write(chunk, 0, bytes);
- }
- } catch (IOException e) {
- return false;
- }
- IOUtils.closeQuietly(from);
- IOUtils.closeQuietly(to);
- return true;
- }
-
- @Nullable
- private List saveMedia(@Nullable ArrayList existingUris) {
- File imageDirectory = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
- File videoDirectory = getExternalFilesDir(Environment.DIRECTORY_MOVIES);
- if (imageDirectory == null || !(imageDirectory.exists() || imageDirectory.mkdirs())) {
- Log.e(TAG, "Image directory is not created.");
- return null;
- }
- if (videoDirectory == null || !(videoDirectory.exists() || videoDirectory.mkdirs())) {
- Log.e(TAG, "Video directory is not created.");
- return null;
- }
- ContentResolver contentResolver = getContentResolver();
- ArrayList filesSoFar = new ArrayList<>();
- ArrayList results = new ArrayList<>();
- for (QueuedMedia item : mediaQueued) {
- /* If the media was already saved in a previous draft, there's no need to save another
- * copy, just add the existing URI to the results. */
- if (existingUris != null) {
- String uri = item.uri.toString();
- int index = existingUris.indexOf(uri);
- if (index != -1) {
- results.add(uri);
- continue;
- }
- }
- // Otherwise, save the media.
- File directory;
- switch (item.type) {
- default:
- case IMAGE:
- directory = imageDirectory;
- break;
- case VIDEO:
- directory = videoDirectory;
- break;
- }
- String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
- .format(new Date());
- String mimeType = contentResolver.getType(item.uri);
- MimeTypeMap map = MimeTypeMap.getSingleton();
- String fileExtension = map.getExtensionFromMimeType(mimeType);
- String filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension);
- File file = new File(directory, filename);
- filesSoFar.add(file);
- boolean copied = copyToFile(contentResolver, item.uri, file);
- if (!copied) {
- /* If any media files were created in prior iterations, delete those before
- * returning. */
- for (File earlierFile : filesSoFar) {
- boolean deleted = earlierFile.delete();
- if (!deleted) {
- Log.i(TAG, "Could not delete the file " + earlierFile.toString());
- }
- }
- return null;
- }
- Uri uri = FileProvider.getUriForFile(this, "com.keylesspalace.tusky.fileprovider",
- file);
- results.add(uri.toString());
- }
- return results;
- }
-
- private void deleteMedia(List mediaUris) {
- for (String uriString : mediaUris) {
- Uri uri = Uri.parse(uriString);
- if (getContentResolver().delete(uri, null, null) == 0) {
- Log.e(TAG, String.format("Did not delete file %s.", uriString));
- }
- }
- }
-
- /**
- * A∖B={x∈A|x∉B}
- *
- * @return all elements of set A that are not in set B.
- */
- private static List setDifference(List a, List b) {
- List c = new ArrayList<>();
- for (String s : a) {
- if (!b.contains(s)) {
- c.add(s);
- }
- }
- return c;
- }
-
- @SuppressLint("StaticFieldLeak")
- private boolean saveTheToot(String s, @Nullable String contentWarning) {
- if (TextUtils.isEmpty(s)) {
- return false;
- }
-
- // Get any existing file's URIs.
- ArrayList existingUris = null;
- String savedJsonUrls = getIntent().getStringExtra("saved_json_urls");
- if (!TextUtils.isEmpty(savedJsonUrls)) {
- existingUris = new Gson().fromJson(savedJsonUrls,
- new TypeToken>() {
- }.getType());
- }
-
- String mediaUrlsSerialized = null;
- if (!ListUtils.isEmpty(mediaQueued)) {
- List savedList = saveMedia(existingUris);
- if (!ListUtils.isEmpty(savedList)) {
- mediaUrlsSerialized = new Gson().toJson(savedList);
- if (!ListUtils.isEmpty(existingUris)) {
- deleteMedia(setDifference(existingUris, savedList));
- }
- } else {
- return false;
- }
- } else if (!ListUtils.isEmpty(existingUris)) {
- /* If there were URIs in the previous draft, but they've now been removed, those files
- * 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() {
- @Override
- protected Void doInBackground(Void... params) {
- tootDao.insertOrReplace(toot);
- return null;
- }
- }.execute();
- return true;
- }
-
- private void addLockToSendButton() {
- floatingBtn.setText(R.string.action_send);
- Drawable lock = AppCompatResources.getDrawable(this, R.drawable.send_private);
- if (lock != null) {
- lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
- floatingBtn.setCompoundDrawables(null, null, lock, null);
- }
+ tootButton.setEnabled(true);
}
private void setStatusVisibility(Status.Visibility visibility) {
statusVisibility = visibility;
+ composeOptionsView.setStatusVisibility(visibility);
+ tootButton.setStatusVisibility(visibility);
+
switch (visibility) {
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);
if (globe != null) {
- visibilityBtn.setImageDrawable(globe);
+ visibilityButton.setImageDrawable(globe);
}
break;
}
case PRIVATE: {
- addLockToSendButton();
Drawable lock = AppCompatResources.getDrawable(this,
R.drawable.ic_lock_outline_24dp);
if (lock != null) {
- visibilityBtn.setImageDrawable(lock);
+ visibilityButton.setImageDrawable(lock);
}
break;
}
case DIRECT: {
- addLockToSendButton();
Drawable envelope = AppCompatResources.getDrawable(this, R.drawable.ic_email_24dp);
if (envelope != null) {
- visibilityBtn.setImageDrawable(envelope);
+ visibilityButton.setImageDrawable(envelope);
}
break;
}
case UNLISTED:
default: {
- floatingBtn.setText(R.string.action_send);
- floatingBtn.setCompoundDrawables(null, null, null, null);
- Drawable openLock = AppCompatResources.getDrawable(this,
- R.drawable.ic_lock_open_24dp);
+ Drawable openLock = AppCompatResources.getDrawable(this, R.drawable.ic_lock_open_24dp);
if (openLock != null) {
- visibilityBtn.setImageDrawable(openLock);
+ visibilityButton.setImageDrawable(openLock);
}
break;
}
@@ -792,59 +695,79 @@ public final class ComposeActivity extends BaseActivity
}
private void showComposeOptions() {
- ComposeOptionsFragment fragment = ComposeOptionsFragment.newInstance(
- statusVisibility, statusHideText);
- fragment.show(getSupportFragmentManager(), null);
+ if (composeOptionsBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN || composeOptionsBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {
+ composeOptionsBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
+ addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
+ emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
+
+ } else {
+ composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
+ }
+ }
+
+ private void showEmojis() {
+ if (emojiBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN || emojiBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {
+ emojiBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
+ composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
+ addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
+
+ } else {
+ emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
+ }
+ }
+
+ private void openPickDialog() {
+ if (addMediaBehavior.getState() == BottomSheetBehavior.STATE_HIDDEN || addMediaBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {
+ addMediaBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
+ composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
+ emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
+
+ } else {
+ addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
+ }
+
+ }
+
+ private void onMediaPick() {
+ addMediaBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
+ PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE);
+ } else {
+ initiateMediaPicking();
+ }
}
@Override
- public void onVisibilityChanged(Status.Visibility visibility) {
+ public void onVisibilityChanged(@NonNull Status.Visibility visibility) {
+ composeOptionsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
setStatusVisibility(visibility);
}
private void updateVisibleCharactersLeft() {
- int left = STATUS_CHARACTER_LIMIT - textEditor.length();
+ int charactersLeft = STATUS_CHARACTER_LIMIT - textEditor.length();
if (statusHideText) {
- left -= contentWarningEditor.length();
+ charactersLeft -= contentWarningEditor.length();
}
- charactersLeft.setText(String.format(Locale.getDefault(), "%d", left));
+ this.charactersLeft.setText(String.format(Locale.getDefault(), "%d", charactersLeft));
}
- public void onContentWarningChanged(boolean hideText) {
- showContentWarning(hideText);
+ private void onContentWarningChanged() {
+ boolean showWarning = contentWarningBar.getVisibility() != View.VISIBLE;
+ showContentWarning(showWarning);
updateVisibleCharactersLeft();
}
- private void setStateToReadying() {
- statusAlreadyInFlight = true;
- disableButtons();
- postProgress.setVisibility(View.VISIBLE);
- }
-
- private void setStateToNotReadying() {
- postProgress.setVisibility(View.INVISIBLE);
- statusAlreadyInFlight = false;
- enableButtons();
- }
-
private void onSendClicked() {
- if (statusAlreadyInFlight) {
- return;
- }
- setStateToReadying();
+ disableButtons();
readyStatus(statusVisibility, statusMarkSensitive);
}
- private void setEditTextMimeTypes() {
- final String[] mimeTypes = new String[]{"image/*"};
- textEditor.setMimeTypes(mimeTypes,
- (inputContentInfo, flags, opts) ->
- ComposeActivity.this.onCommitContent(inputContentInfo, flags,
- mimeTypes));
- }
-
- private boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags,
- String[] mimeTypes) {
+ @Override
+ public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts) {
try {
if (currentInputContentInfo != null) {
currentInputContentInfo.releasePermission();
@@ -855,14 +778,8 @@ public final class ComposeActivity extends BaseActivity
currentInputContentInfo = null;
}
- // Verify the returned content's type is actually in the list of MIME types requested.
- boolean supported = false;
- for (final String mimeType : mimeTypes) {
- if (inputContentInfo.getDescription().hasMimeType(mimeType)) {
- supported = true;
- break;
- }
- }
+ // Verify the returned content's type is of the correct MIME type
+ boolean supported = inputContentInfo.getDescription().hasMimeType("image/*");
return supported && onCommitContentInternal(inputContentInfo, flags);
}
@@ -908,68 +825,23 @@ public final class ComposeActivity extends BaseActivity
private void sendStatus(String content, Status.Visibility visibility, boolean sensitive,
String spoilerText) {
ArrayList mediaIds = new ArrayList<>();
-
+ ArrayList mediaUris = new ArrayList<>();
for (QueuedMedia item : mediaQueued) {
mediaIds.add(item.id);
+ mediaUris.add(item.uri.toString());
}
- Callback callback = new Callback() {
- @Override
- public void onResponse(@NonNull Call call, @NonNull Response response) {
- if (response.isSuccessful()) {
- onSendSuccess();
- } else {
- onSendFailure(response);
- }
- }
+ Intent sendIntent = SendTootService.sendTootIntent(this, content, spoilerText,
+ visibility, sensitive, mediaIds, mediaUris, inReplyToId,
+ getIntent().getStringExtra(REPLYING_STATUS_CONTENT_EXTRA),
+ getIntent().getStringExtra(REPLYING_STATUS_AUTHOR_USERNAME_EXTRA),
+ getIntent().getStringExtra(SAVED_JSON_URLS_EXTRA),
+ accountManager.getActiveAccount(), savedTootUid);
- @Override
- public void onFailure(@NonNull Call call, @NonNull Throwable t) {
- onSendFailure(null);
- }
- };
- mastodonApi.createStatus(content, inReplyToId, spoilerText, visibility.serverString(),
- sensitive, mediaIds).enqueue(callback);
- }
+ startService(sendIntent);
- private void onSendSuccess() {
- // If the status was loaded from a draft, delete the draft and associated media files.
- if (savedTootUid != 0) {
- tootDao.delete(savedTootUid);
- for (QueuedMedia item : mediaQueued) {
- try {
- if (getContentResolver().delete(item.uri, null, null) == 0) {
- Log.e(TAG, String.format("Did not delete file %s.", item.uri.toString()));
- }
- } catch (SecurityException e) {
- Log.e(TAG, String.format("Did not delete file %s.", item.uri.toString()), e);
- }
-
- }
- }
- Snackbar bar = Snackbar.make(findViewById(R.id.activity_compose),
- getString(R.string.confirmation_send), Snackbar.LENGTH_SHORT);
- bar.show();
- setResult(COMPOSE_SUCCESS);
finish();
- }
- private void onSendFailure(@Nullable Response 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, (dialog, 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 Status.Visibility visibility, final boolean sensitive) {
@@ -1003,7 +875,7 @@ public final class ComposeActivity extends BaseActivity
@Override
protected void onCancelled() {
removeAllMediaFromQueue();
- setStateToNotReadying();
+ enableButtons();
super.onCancelled();
}
};
@@ -1026,56 +898,22 @@ public final class ComposeActivity extends BaseActivity
spoilerText = contentWarningEditor.getText().toString();
}
int characterCount = contentText.length() + spoilerText.length();
- if (characterCount > 0 && characterCount <= STATUS_CHARACTER_LIMIT) {
- sendStatus(contentText, visibility, sensitive, spoilerText);
- } else if (characterCount <= 0) {
+ if (characterCount <= 0 && mediaQueued.size()==0) {
textEditor.setError(getString(R.string.error_empty));
- setStateToNotReadying();
+ enableButtons();
+ } else if (characterCount <= STATUS_CHARACTER_LIMIT) {
+ sendStatus(contentText, visibility, sensitive, spoilerText);
+
} else {
textEditor.setError(getString(R.string.error_compose_character_limit));
- setStateToNotReadying();
+ enableButtons();
}
}
private void onReadyFailure(final Status.Visibility visibility, final boolean sensitive) {
doErrorDialog(R.string.error_media_upload_sending, R.string.action_retry,
v -> readyStatus(visibility, sensitive));
- setStateToNotReadying();
- }
-
- private void openPickDialog() {
- final int CHOICE_TAKE = 0;
- final int CHOICE_PICK = 1;
- CharSequence[] choices = new CharSequence[2];
- choices[CHOICE_TAKE] = getString(R.string.action_photo_take);
- choices[CHOICE_PICK] = getString(R.string.action_photo_pick);
- DialogInterface.OnClickListener listener = (dialog, which) -> {
- switch (which) {
- case CHOICE_TAKE: {
- initiateCameraApp();
- break;
- }
- case CHOICE_PICK: {
- onMediaPick();
- break;
- }
- }
- };
- AlertDialog dialog = new AlertDialog.Builder(this)
- .setItems(choices, listener)
- .create();
- dialog.show();
- }
-
- private void onMediaPick() {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
- != PackageManager.PERMISSION_GRANTED) {
- ActivityCompat.requestPermissions(this,
- new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
- PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE);
- } else {
- initiateMediaPicking();
- }
+ enableButtons();
}
@Override
@@ -1109,6 +947,8 @@ public final class ComposeActivity extends BaseActivity
}
private void initiateCameraApp() {
+ addMediaBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+
// We don't need to ask for permission in this case, because the used calls require
// android.permission.WRITE_EXTERNAL_STORAGE only on SDKs *older* than Kitkat, which was
// way before permission dialogues have been introduced.
@@ -1123,7 +963,7 @@ public final class ComposeActivity extends BaseActivity
// Continue only if the File was successfully created
if (photoFile != null) {
photoUploadUri = FileProvider.getUriForFile(this,
- "com.keylesspalace.tusky.fileprovider",
+ BuildConfig.APPLICATION_ID+".fileprovider",
photoFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUploadUri);
startActivityForResult(intent, MEDIA_TAKE_PHOTO_RESULT);
@@ -1132,7 +972,12 @@ public final class ComposeActivity extends BaseActivity
}
private void initiateMediaPicking() {
- Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ Intent intent;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ } else {
+ intent = new Intent(Intent.ACTION_GET_CONTENT);
+ }
intent.addCategory(Intent.CATEGORY_OPENABLE);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
intent.setType("image/* video/*");
@@ -1147,7 +992,7 @@ public final class ComposeActivity extends BaseActivity
private void enableMediaButtons() {
pickButton.setEnabled(true);
ThemeUtils.setDrawableTint(this, pickButton.getDrawable(),
- R.attr.compose_media_button_tint);
+ android.R.attr.textColorTertiary);
}
private void disableMediaButtons() {
@@ -1178,12 +1023,6 @@ public final class ComposeActivity extends BaseActivity
mediaQueued.add(item);
int queuedCount = mediaQueued.size();
if (queuedCount == 1) {
- /* The media preview bar is actually not inset in the EditText, it just overlays it and
- * is aligned to the bottom. But, so that text doesn't get hidden under it, extra
- * padding is added at the bottom of the EditText. */
- int totalHeight = side + margin + marginBottom;
- textEditor.setPadding(textEditor.getPaddingLeft(), textEditor.getPaddingTop(),
- textEditor.getPaddingRight(), totalHeight);
// If there's one video in the queue it is full, so disable the button to queue more.
if (item.type == QueuedMedia.Type.VIDEO) {
disableMediaButtons();
@@ -1192,9 +1031,9 @@ public final class ComposeActivity extends BaseActivity
// Limit the total media attachments, also.
disableMediaButtons();
}
- if (queuedCount >= 1) {
- showMarkSensitive(true);
- }
+
+ updateHideMediaToggle();
+
if (item.readyStage != QueuedMedia.ReadyStage.UPLOADED) {
waitForMediaLatch.countUp();
if (mediaSize > STATUS_MEDIA_SIZE_LIMIT && type == QueuedMedia.Type.IMAGE) {
@@ -1279,7 +1118,6 @@ public final class ComposeActivity extends BaseActivity
Window window = dialog.getWindow();
if (window != null) {
- //noinspection ConstantConditions
window.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
}
@@ -1295,13 +1133,9 @@ public final class ComposeActivity extends BaseActivity
mediaPreviewBar.removeView(item.preview);
mediaQueued.remove(item);
if (mediaQueued.size() == 0) {
- showMarkSensitive(false);
- /* If there are no image previews to show, the extra padding that was added to the
- * EditText can be removed so there isn't unnecessary empty space. */
- textEditor.setPadding(textEditor.getPaddingLeft(), textEditor.getPaddingTop(),
- textEditor.getPaddingRight(), 0);
+ updateHideMediaToggle();
}
- textEditor.setText(removeUrlFromEditable(textEditor.getEditableText(), item.uploadUrl));
+
enableMediaButtons();
cancelReadyingMedia(item);
}
@@ -1314,19 +1148,6 @@ public final class ComposeActivity extends BaseActivity
}
}
- private static Editable removeUrlFromEditable(Editable editable, @Nullable URLSpan urlSpan) {
- if (urlSpan == null) {
- return editable;
- }
- SpannableStringBuilder builder = new SpannableStringBuilder(editable);
- int start = builder.getSpanStart(urlSpan);
- int end = builder.getSpanEnd(urlSpan);
- if (start != -1 && end != -1) {
- builder.delete(start, end);
- }
- return builder;
- }
-
private void downsizeMedia(final QueuedMedia item) {
item.readyStage = QueuedMedia.ReadyStage.DOWNSIZING;
@@ -1428,19 +1249,6 @@ public final class ComposeActivity extends BaseActivity
item.preview.setProgress(-1);
item.readyStage = QueuedMedia.ReadyStage.UPLOADED;
- /* Add the upload URL to the text field. Also, keep a reference to the span so if the user
- * chooses to remove the media, the URL is also automatically removed. */
- item.uploadUrl = new URLSpan(media.getTextUrl());
- int end = 1 + media.getTextUrl().length();
- SpannableStringBuilder builder = new SpannableStringBuilder();
- builder.append(' ');
- builder.append(media.getTextUrl());
- builder.setSpan(item.uploadUrl, 1, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- int cursorStart = textEditor.getSelectionStart();
- int cursorEnd = textEditor.getSelectionEnd();
- textEditor.append(builder);
- textEditor.setSelection(cursorStart, cursorEnd);
-
waitForMediaLatch.countDown();
}
@@ -1471,10 +1279,15 @@ public final class ComposeActivity extends BaseActivity
}
@Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (resultCode == RESULT_OK && requestCode == MEDIA_PICK_RESULT && data != null) {
- Uri uri = data.getData();
+ protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ super.onActivityResult(requestCode, resultCode, intent);
+ if (resultCode == RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) {
+ Uri uri = intent.getData();
+ if (uri != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ // this is necessary so the SendTootService can access the uri later
+ final int takeFlags = intent.getFlags() & Intent.FLAG_GRANT_READ_URI_PERMISSION;
+ getContentResolver().takePersistableUriPermission(uri, takeFlags);
+ }
long mediaSize = MediaUtils.getMediaSize(getContentResolver(), uri);
pickMedia(uri, mediaSize);
} else if (resultCode == RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) {
@@ -1531,29 +1344,19 @@ public final class ComposeActivity extends BaseActivity
}
}
- private void showMarkSensitive(boolean show) {
- showMarkSensitive = show;
-
- if (!showMarkSensitive) {
- statusMarkSensitive = false;
- ThemeUtils.setDrawableTint(this, hideMediaToggle.getDrawable(),
- R.attr.compose_hide_media_button_color);
- }
-
- if (show) {
- hideMediaToggle.setVisibility(View.VISIBLE);
- } else {
- hideMediaToggle.setVisibility(View.GONE);
- }
- }
-
private void showContentWarning(boolean show) {
statusHideText = show;
+ TransitionManager.beginDelayedTransition((ViewGroup)contentWarningBar.getParent());
if (show) {
+ statusMarkSensitive = true;
contentWarningBar.setVisibility(View.VISIBLE);
+ contentWarningButton.setTextColor(ContextCompat.getColor(this, R.color.primary));
} else {
contentWarningBar.setVisibility(View.GONE);
+ contentWarningButton.setTextColor(ThemeUtils.getColor(this, android.R.attr.textColorTertiary));
}
+ updateHideMediaToggle();
+
}
@Override
@@ -1578,15 +1381,33 @@ public final class ComposeActivity extends BaseActivity
|| !TextUtils.isEmpty(contentWarningEditor.getText())
|| !mediaQueued.isEmpty()) {
new AlertDialog.Builder(this)
- .setTitle("Close the toot without saving?")
- .setPositiveButton(android.R.string.yes, (d, w) -> finish())
- .setNegativeButton(android.R.string.no, null)
+ .setMessage(R.string.compose_save_draft)
+ .setPositiveButton(R.string.action_save, (d, w) -> saveDraftAndFinish())
+ .setNegativeButton(R.string.action_delete, (d, w) -> finish())
.show();
} else {
finish();
}
}
+ private void saveDraftAndFinish() {
+ ArrayList mediaUris = new ArrayList<>();
+ for (QueuedMedia item : mediaQueued) {
+ mediaUris.add(item.uri.toString());
+ }
+
+ saveTootHelper.saveToot(textEditor.getText().toString(),
+ contentWarningEditor.getText().toString(),
+ getIntent().getStringExtra("saved_json_urls"),
+ mediaUris,
+ savedTootUid,
+ inReplyToId,
+ getIntent().getStringExtra(REPLYING_STATUS_CONTENT_EXTRA),
+ getIntent().getStringExtra(REPLYING_STATUS_AUTHOR_USERNAME_EXTRA),
+ statusVisibility);
+ finish();
+ }
+
@Override
public List searchAccounts(String mention) {
ArrayList resultList = new ArrayList<>();
@@ -1603,13 +1424,17 @@ public final class ComposeActivity extends BaseActivity
return resultList;
}
- private static final class QueuedMedia {
+ @Override
+ public void onEmojiSelected(@NotNull String shortcode) {
+ textEditor.getText().insert(textEditor.getSelectionStart(), ":"+shortcode+": ");
+ }
+
+ public static final class QueuedMedia {
Type type;
ProgressImageView preview;
Uri uri;
String id;
Call uploadRequest;
- URLSpan uploadUrl;
ReadyStage readyStage;
byte[] content;
long mediaSize;
@@ -1624,7 +1449,7 @@ public final class ComposeActivity extends BaseActivity
this.description = description;
}
- enum Type {
+ public enum Type {
IMAGE,
VIDEO
}
@@ -1687,7 +1512,6 @@ public final class ComposeActivity extends BaseActivity
}
}
- @SuppressWarnings("WeakerAccess")
public static final class IntentBuilder {
@Nullable
private Integer savedTootUid;
diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java
index bf45bd968..b24517a61 100644
--- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java
+++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java
@@ -29,7 +29,6 @@ import android.support.design.widget.TabLayout;
import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
-import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AlertDialog;
import android.util.Log;
@@ -43,7 +42,6 @@ import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.pager.TimelinePagerAdapter;
-import com.keylesspalace.tusky.receiver.TimelineReceiver;
import com.keylesspalace.tusky.util.NotificationHelper;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
@@ -242,16 +240,6 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity,
}
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (requestCode == COMPOSE_RESULT && resultCode == ComposeActivity.RESULT_OK) {
- Intent intent = new Intent(TimelineReceiver.Types.STATUS_COMPOSED);
- LocalBroadcastManager.getInstance(getApplicationContext())
- .sendBroadcast(intent);
- }
- super.onActivityResult(requestCode, resultCode, data);
- }
-
@Override
public void onBackPressed() {
if (drawer != null && drawer.isDrawerOpen()) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java
index cd1d60573..d0b2dd615 100644
--- a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java
+++ b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java
@@ -15,27 +15,29 @@
package com.keylesspalace.tusky;
+import android.content.BroadcastReceiver;
+import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
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.v4.content.LocalBroadcastManager;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
-import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
-import com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
import com.keylesspalace.tusky.adapter.SavedTootAdapter;
import com.keylesspalace.tusky.db.TootDao;
import com.keylesspalace.tusky.db.TootEntity;
+import com.keylesspalace.tusky.receiver.TimelineReceiver;
+import com.keylesspalace.tusky.util.SaveTootHelper;
import com.keylesspalace.tusky.util.ThemeUtils;
import java.lang.ref.WeakReference;
@@ -43,11 +45,12 @@ import java.util.ArrayList;
import java.util.List;
public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.SavedTootAction {
- private static final String TAG = "SavedTootActivity"; // logging tag
// dao
private static TootDao tootDao = TuskyApplication.getDB().tootDao();
+ private SaveTootHelper saveTootHelper;
+
// ui
private SavedTootAdapter adapter;
private TextView noContent;
@@ -55,9 +58,27 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.
private List toots = new ArrayList<>();
@Nullable private AsyncTask, ?, ?> asyncTask;
+ private BroadcastReceiver broadcastReceiver;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
+ saveTootHelper = new SaveTootHelper(tootDao, this);
+
+ broadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ fetchToots();
+ }
+ };
+
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(TimelineReceiver.Types.STATUS_COMPOSED);
+
+ LocalBroadcastManager.getInstance(this)
+ .registerReceiver(broadcastReceiver, intentFilter);
+
setContentView(R.layout.activity_saved_toot);
Toolbar toolbar = findViewById(R.id.toolbar);
@@ -96,6 +117,12 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.
if (asyncTask != null) asyncTask.cancel(true);
}
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver);
+ }
+
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
@@ -122,19 +149,9 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.
@Override
public void delete(int position, TootEntity item) {
- // Delete any media files associated with the status.
- ArrayList uris = new Gson().fromJson(item.getUrls(),
- new TypeToken>() {}.getType());
- if (uris != null) {
- for (String uriString : uris) {
- Uri uri = Uri.parse(uriString);
- if (getContentResolver().delete(uri, null, null) == 0) {
- Log.e(TAG, String.format("Did not delete file %s.", uriString));
- }
- }
- }
- // update DB
- tootDao.delete(item.getUid());
+
+ saveTootHelper.deleteDraft(item);
+
toots.remove(position);
// update adapter
if (adapter != null) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java
index db04465bf..4d6089c20 100644
--- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java
+++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.java
@@ -17,6 +17,7 @@ package com.keylesspalace.tusky;
import android.app.Activity;
import android.app.Application;
+import android.app.Service;
import android.app.UiModeManager;
import android.arch.persistence.room.Room;
import android.content.Context;
@@ -39,10 +40,11 @@ import javax.inject.Inject;
import dagger.android.AndroidInjector;
import dagger.android.DispatchingAndroidInjector;
import dagger.android.HasActivityInjector;
+import dagger.android.HasServiceInjector;
import okhttp3.Cache;
import okhttp3.OkHttpClient;
-public class TuskyApplication extends Application implements HasActivityInjector {
+public class TuskyApplication extends Application implements HasActivityInjector, HasServiceInjector {
public static final String APP_THEME_DEFAULT = ThemeUtils.THEME_NIGHT;
private static AppDatabase db;
@@ -50,6 +52,8 @@ public class TuskyApplication extends Application implements HasActivityInjector
@Inject
DispatchingAndroidInjector dispatchingAndroidInjector;
@Inject
+ DispatchingAndroidInjector dispatchingServiceInjector;
+ @Inject
NotificationPullJobCreator notificationPullJobCreator;
public static AppDatabase getDB() {
@@ -75,7 +79,7 @@ public class TuskyApplication extends Application implements HasActivityInjector
db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "tuskyDB")
.allowMainThreadQueries()
- .addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5)
+ .addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5, AppDatabase.MIGRATION_5_6)
.build();
accountManager = new AccountManager(db);
serviceLocator = new ServiceLocator() {
@@ -90,7 +94,7 @@ public class TuskyApplication extends Application implements HasActivityInjector
}
};
- AppInjector.INSTANCE.init(this);
+ initAppInjector();
initPicasso();
JobManager.create(this).addJobCreator(notificationPullJobCreator);
@@ -100,6 +104,10 @@ public class TuskyApplication extends Application implements HasActivityInjector
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
+ protected void initAppInjector() {
+ AppInjector.INSTANCE.init(this);
+ }
+
protected void initPicasso() {
// Initialize Picasso configuration
Picasso.Builder builder = new Picasso.Builder(this);
@@ -128,6 +136,11 @@ public class TuskyApplication extends Application implements HasActivityInjector
return dispatchingAndroidInjector;
}
+ @Override
+ public AndroidInjector serviceInjector() {
+ return dispatchingServiceInjector;
+ }
+
public interface ServiceLocator {
T get(Class clazz);
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt
new file mode 100644
index 000000000..61e131b4e
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt
@@ -0,0 +1,55 @@
+/* Copyright 2018 Conny Duck
+ *
+ * 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 . */
+
+package com.keylesspalace.tusky.adapter
+
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.ImageView
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.entity.Emoji
+import com.squareup.picasso.Picasso
+
+class EmojiAdapter(private val emojiList: List, private val onEmojiSelectedListener: OnEmojiSelectedListener) : RecyclerView.Adapter() {
+
+ override fun getItemCount(): Int {
+ return emojiList.size
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmojiAdapter.EmojiHolder {
+
+ val view = LayoutInflater.from(parent.context).inflate(R.layout.item_emoji_button, parent, false) as ImageView
+ return EmojiHolder(view)
+
+ }
+
+ override fun onBindViewHolder(viewHolder: EmojiAdapter.EmojiHolder, position: Int) {
+ Picasso.with(viewHolder.emojiImageView.context)
+ .load(emojiList[position].url)
+ .into(viewHolder.emojiImageView)
+
+ viewHolder.emojiImageView.setOnClickListener {
+ onEmojiSelectedListener.onEmojiSelected(emojiList[position].shortcode)
+ }
+ }
+
+ class EmojiHolder(val emojiImageView: ImageView) : RecyclerView.ViewHolder(emojiImageView)
+
+}
+
+interface OnEmojiSelectedListener {
+ fun onEmojiSelected(shortcode: String)
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java
index 694230124..3f727c8fc 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java
@@ -37,8 +37,8 @@ import android.widget.TextView;
import android.widget.ToggleButton;
import com.keylesspalace.tusky.R;
+import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Notification;
-import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.LinkListener;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
@@ -485,7 +485,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
}
Spanned content = statusViewData.getContent();
- List emojis = statusViewData.getEmojis();
+ List emojis = statusViewData.getEmojis();
Spanned emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, statusContent);
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java
index 6c51e74d1..4f2bedbf3 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java
@@ -19,6 +19,7 @@ import android.widget.ToggleButton;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Attachment;
+import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
@@ -106,7 +107,7 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
username.setText(usernameText);
}
- private void setContent(Spanned content, Status.Mention[] mentions, List emojis,
+ private void setContent(Spanned content, Status.Mention[] mentions, List emojis,
StatusActionListener listener) {
Spanned emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, this.content);
@@ -384,7 +385,7 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
sensitiveMediaShow.setVisibility(View.GONE);
}
- private void setSpoilerText(String spoilerText, List emojis,
+ private void setSpoilerText(String spoilerText, List emojis,
final boolean expanded, final StatusActionListener listener) {
CharSequence emojiSpoiler =
CustomEmojiHelper.emojifyString(spoilerText, emojis, contentWarningDescription);
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt
index e295ee1fd..7e6c5c736 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt
@@ -15,9 +15,7 @@
package com.keylesspalace.tusky.db
-import android.arch.persistence.room.Database
import android.util.Log
-import com.keylesspalace.tusky.TuskyApplication
import com.keylesspalace.tusky.entity.Account
/**
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
index 54bf45946..e6e50fda8 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
+++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
@@ -25,11 +25,12 @@ import android.support.annotation.NonNull;
* DB version & declare DAO
*/
-@Database(entities = {TootEntity.class, AccountEntity.class}, version = 5, exportSchema = false)
+@Database(entities = {TootEntity.class, AccountEntity.class, EmojiListEntity.class}, version = 6, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
public abstract TootDao tootDao();
public abstract AccountDao accountDao();
+ public abstract EmojiListDao emojiListDao();
public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
@@ -74,4 +75,11 @@ public abstract class AppDatabase extends RoomDatabase {
database.execSQL("CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `AccountEntity` (`domain`, `accountId`)");
}
};
+
+ public static final Migration MIGRATION_5_6 = new Migration(5, 6) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
+ database.execSQL("CREATE TABLE IF NOT EXISTS `EmojiListEntity` (`instance` TEXT NOT NULL, `emojiList` TEXT NOT NULL, PRIMARY KEY(`instance`))");
+ }
+ };
}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/EmojiListDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/EmojiListDao.kt
new file mode 100644
index 000000000..8630b1656
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/db/EmojiListDao.kt
@@ -0,0 +1,31 @@
+/* Copyright 2018 Conny Duck
+ *
+ * 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 . */
+
+package com.keylesspalace.tusky.db
+
+import android.arch.persistence.room.Dao
+import android.arch.persistence.room.Insert
+import android.arch.persistence.room.OnConflictStrategy
+import android.arch.persistence.room.Query
+
+@Dao
+interface EmojiListDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun insertOrReplace(emojiList: EmojiListEntity)
+
+ @Query("SELECT * FROM EmojiListEntity WHERE instance = :instance LIMIT 1")
+ fun loadEmojisForInstance(instance: String): EmojiListEntity?
+
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/EmojiListEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/EmojiListEntity.kt
new file mode 100644
index 000000000..ac87e8bf6
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/db/EmojiListEntity.kt
@@ -0,0 +1,43 @@
+/* Copyright 2018 Conny Duck
+ *
+ * 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 . */
+
+package com.keylesspalace.tusky.db
+
+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 com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import com.keylesspalace.tusky.entity.Emoji
+
+@Entity
+@TypeConverters(Converters::class)
+data class EmojiListEntity(@field:PrimaryKey var instance: String,
+ val emojiList: List)
+
+
+class Converters {
+
+ @TypeConverter
+ fun jsonToList(emojiListJson: String): List {
+ return Gson().fromJson(emojiListJson, object : TypeToken>() {}.type)
+ }
+
+ @TypeConverter
+ fun listToJson(emojiList: List): String {
+ return Gson().toJson(emojiList)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java b/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java
index bec94622a..d84721630 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java
+++ b/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java
@@ -38,4 +38,7 @@ public interface TootDao {
@Query("DELETE FROM TootEntity WHERE uid = :uid")
int delete(int uid);
+
+ @Query("SELECT * FROM TootEntity WHERE uid = :uid")
+ TootEntity find(int uid);
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt
index f06186763..b2ad4d12d 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/AppComponent.kt
@@ -31,7 +31,8 @@ import javax.inject.Singleton
AppModule::class,
NetworkModule::class,
AndroidInjectionModule::class,
- ActivitiesModule::class
+ ActivitiesModule::class,
+ ServicesModule::class
])
interface AppComponent {
@Component.Builder
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
index cb02adf2a..fdf7d8dfc 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
@@ -21,7 +21,6 @@ import android.content.Context
import android.content.SharedPreferences
import android.preference.PreferenceManager
import android.support.v4.content.LocalBroadcastManager
-import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.TuskyApplication
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.network.MastodonApi
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ServicesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ServicesModule.kt
new file mode 100644
index 000000000..f024df6b2
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/di/ServicesModule.kt
@@ -0,0 +1,26 @@
+/* Copyright 2018 Conny Duck
+ *
+ * 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 . */
+
+package com.keylesspalace.tusky.di
+
+import com.keylesspalace.tusky.service.SendTootService
+import dagger.Module
+import dagger.android.ContributesAndroidInjector
+
+@Module
+abstract class ServicesModule {
+ @ContributesAndroidInjector
+ abstract fun contributeMyService(): SendTootService
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt
new file mode 100644
index 000000000..1c0cb4dc0
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt
@@ -0,0 +1,25 @@
+/* Copyright 2018 Conny Duck
+ *
+ * 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 . */
+
+package com.keylesspalace.tusky.entity
+
+import android.os.Parcelable
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+data class Emoji(
+ val shortcode: String,
+ val url: String
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt
index 8e687e49a..09096e5be 100644
--- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt
@@ -131,11 +131,6 @@ data class Status(
var website: String? = null
}
- class Emoji {
- val shortcode: String? = null
- val url: String? = null
- }
-
companion object {
const val MAX_MEDIA_ATTACHMENTS = 4
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java
index 862c11095..01d331281 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java
@@ -31,7 +31,6 @@ import android.view.View;
import android.view.ViewGroup;
import com.keylesspalace.tusky.AccountActivity;
-import com.keylesspalace.tusky.BaseActivity;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.adapter.AccountAdapter;
import com.keylesspalace.tusky.adapter.BlocksAdapter;
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ComposeOptionsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ComposeOptionsFragment.java
deleted file mode 100644
index 6fbbd47d2..000000000
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/ComposeOptionsFragment.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/* 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 . */
-
-package com.keylesspalace.tusky.fragment;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.Bundle;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.Nullable;
-import android.support.design.widget.BottomSheetDialogFragment;
-import android.support.graphics.drawable.VectorDrawableCompat;
-import android.support.v4.graphics.drawable.DrawableCompat;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CheckBox;
-import android.widget.CompoundButton;
-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(Status.Visibility visibility);
- void onContentWarningChanged(boolean hideText);
- }
-
- private RadioGroup radio;
- private CheckBox hideText;
- private Listener listener;
-
- public static ComposeOptionsFragment newInstance(Status.Visibility visibility, boolean hideText) {
- Bundle arguments = new Bundle();
- ComposeOptionsFragment fragment = new ComposeOptionsFragment();
- arguments.putInt("visibilityNum", visibility.getNum());
- arguments.putBoolean("hideText", hideText);
- fragment.setArguments(arguments);
- return fragment;
- }
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
- listener = (Listener) context;
- }
-
- @Nullable
- @Override
- public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- View rootView = inflater.inflate(R.layout.fragment_compose_options, container, false);
-
- Bundle arguments = getArguments();
- 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;
- 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);
-
- RadioButton publicButton = rootView.findViewById(R.id.radio_public);
- RadioButton unlistedButton = rootView.findViewById(R.id.radio_unlisted);
- RadioButton privateButton = rootView.findViewById(R.id.radio_private);
- RadioButton directButton = rootView.findViewById(R.id.radio_direct);
- setRadioButtonDrawable(getContext(), publicButton, R.drawable.ic_public_24dp);
- setRadioButtonDrawable(getContext(), unlistedButton, R.drawable.ic_lock_open_24dp);
- setRadioButtonDrawable(getContext(), privateButton, R.drawable.ic_lock_outline_24dp);
- setRadioButtonDrawable(getContext(), directButton, R.drawable.ic_email_24dp);
-
- hideText = rootView.findViewById(R.id.compose_hide_text);
- hideText.setChecked(statusHideText);
-
- return rootView;
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- radio.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(RadioGroup group, int checkedId) {
- Status.Visibility visibility;
- switch (checkedId) {
- default:
- case R.id.radio_public: {
- visibility = Status.Visibility.PUBLIC;
- break;
- }
- case R.id.radio_unlisted: {
- visibility = Status.Visibility.UNLISTED;
- break;
- }
- case R.id.radio_private: {
- visibility = Status.Visibility.PRIVATE;
- break;
- }
- case R.id.radio_direct: {
- visibility = Status.Visibility.DIRECT;
- break;
- }
- }
- listener.onVisibilityChanged(visibility);
- }
- });
- hideText.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
- listener.onContentWarningChanged(isChecked);
- }
- });
- }
-
- private static void setRadioButtonDrawable(Context context, RadioButton button,
- @DrawableRes int id) {
- ColorStateList list = new ColorStateList(new int[][] {
- new int[] { -android.R.attr.state_checked },
- new int[] { android.R.attr.state_checked }
- }, new int[] {
- ThemeUtils.getColor(context, R.attr.compose_image_button_tint),
- ThemeUtils.getColor(context, R.attr.colorAccent)
- });
- Drawable drawable = VectorDrawableCompat.create(context.getResources(), id,
- context.getTheme());
- if (drawable == null) {
- return;
- }
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- button.setButtonTintList(list);
- } else {
- drawable = DrawableCompat.wrap(drawable);
- DrawableCompat.setTintList(drawable, list);
- }
- button.setButtonDrawable(drawable);
- }
-}
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
index 8b23caa86..c17a58b2d 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
@@ -20,18 +20,14 @@ import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
-import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityOptionsCompat;
-import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.PopupMenu;
import android.text.Spanned;
-import android.view.MenuItem;
import android.view.View;
import com.keylesspalace.tusky.AccountActivity;
-import com.keylesspalace.tusky.BaseActivity;
import com.keylesspalace.tusky.ComposeActivity;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.ReportActivity;
@@ -41,27 +37,16 @@ import com.keylesspalace.tusky.ViewTagActivity;
import com.keylesspalace.tusky.ViewThreadActivity;
import com.keylesspalace.tusky.ViewVideoActivity;
import com.keylesspalace.tusky.db.AccountEntity;
-import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.entity.Attachment;
-import com.keylesspalace.tusky.entity.Relationship;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.AdapterItemRemover;
-import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.network.TimelineCases;
-import com.keylesspalace.tusky.receiver.TimelineReceiver;
import com.keylesspalace.tusky.util.HtmlUtils;
import java.util.ArrayList;
import java.util.List;
-import javax.inject.Inject;
-
-import okhttp3.ResponseBody;
-import retrofit2.Call;
-import retrofit2.Callback;
-import retrofit2.Response;
-
/* Note from Andrew on Jan. 22, 2017: This class is a design problem for me, so I left it with an
* awkward name. TimelineFragment and NotificationFragment have significant overlap but the nature
* of that is complicated by how they're coupled with Status and Notification and the corresponding
diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java
index 06f2aaaa1..74fde5878 100644
--- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java
+++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java
@@ -22,6 +22,7 @@ import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.AppCredentials;
import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Card;
+import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.MastoList;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Relationship;
@@ -101,12 +102,15 @@ public interface MastodonApi {
@FormUrlEncoded
@POST("api/v1/statuses")
Call createStatus(
+ @Header("Authorization") String auth,
+ @Header(DOMAIN_HEADER) String domain,
@Field("status") String text,
@Field("in_reply_to_id") String inReplyToId,
@Field("spoiler_text") String warningText,
@Field("visibility") String visibility,
@Field("sensitive") Boolean sensitive,
- @Field("media_ids[]") List mediaIds);
+ @Field("media_ids[]") List mediaIds,
+ @Header("Idempotency-Key") String idempotencyKey);
@GET("api/v1/statuses/{id}")
Call status(@Path("id") String statusId);
@GET("api/v1/statuses/{id}/context")
@@ -263,4 +267,7 @@ public interface MastodonApi {
@GET("/api/v1/lists")
Call> getLists();
+
+ @GET("/api/v1/custom_emojis")
+ Call> getCustomEmojis();
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt
new file mode 100644
index 000000000..cf919219c
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt
@@ -0,0 +1,329 @@
+package com.keylesspalace.tusky.service
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.IBinder
+import android.os.Parcelable
+import android.support.v4.app.NotificationCompat
+import android.support.v4.app.ServiceCompat
+import android.support.v4.content.ContextCompat
+import android.support.v4.content.LocalBroadcastManager
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.TuskyApplication
+import com.keylesspalace.tusky.db.AccountEntity
+import com.keylesspalace.tusky.db.AccountManager
+import com.keylesspalace.tusky.di.Injectable
+import com.keylesspalace.tusky.entity.Status
+import com.keylesspalace.tusky.network.MastodonApi
+import com.keylesspalace.tusky.receiver.TimelineReceiver
+import com.keylesspalace.tusky.util.SaveTootHelper
+import com.keylesspalace.tusky.util.StringUtils
+import dagger.android.AndroidInjection
+import kotlinx.android.parcel.Parcelize
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+import java.util.*
+import java.util.concurrent.ConcurrentHashMap
+import javax.inject.Inject
+
+class SendTootService: Service(), Injectable {
+
+ @Inject
+ lateinit var mastodonApi: MastodonApi
+ @Inject
+ lateinit var accountManager: AccountManager
+
+ private lateinit var saveTootHelper: SaveTootHelper
+
+ private val tootsToSend = ConcurrentHashMap()
+ private val sendCalls = ConcurrentHashMap>()
+
+ private val timer = Timer()
+
+
+ override fun onCreate() {
+ AndroidInjection.inject(this)
+ saveTootHelper = SaveTootHelper(TuskyApplication.getDB().tootDao(), this)
+ super.onCreate()
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ return null
+ }
+
+ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+
+ if(intent.hasExtra(KEY_TOOT)) {
+
+ val tootToSend = intent.getParcelableExtra(KEY_TOOT)
+
+ if (tootToSend == null) {
+ throw IllegalStateException("SendTootService started without $KEY_TOOT extra")
+ }
+
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(CHANNEL_ID, getString(R.string.send_toot_notification_channel_name), NotificationManager.IMPORTANCE_LOW)
+ notificationManager.createNotificationChannel(channel)
+
+ }
+
+ var notificationText = tootToSend.warningText
+ if (notificationText.isBlank()) {
+ notificationText = tootToSend.text
+ }
+
+ val builder = NotificationCompat.Builder(this, CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_notify)
+ .setContentTitle(getString(R.string.send_toot_notification_title))
+ .setContentText(notificationText)
+ .setProgress(1, 0, true)
+ .setOngoing(true)
+ .setColor(ContextCompat.getColor(this, R.color.primary))
+ .addAction(0, getString(android.R.string.cancel), cancelSendingIntent(notificationId))
+
+ if(tootsToSend.size == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
+ startForeground(notificationId, builder.build())
+ } else {
+ notificationManager.notify(notificationId, builder.build())
+ }
+
+ tootsToSend[notificationId] = tootToSend
+ sendToot(notificationId)
+
+ notificationId--
+
+ } else {
+
+ if(intent.hasExtra(KEY_CANCEL)) {
+ cancelSending(intent.getIntExtra(KEY_CANCEL, 0))
+ stopSelf(intent.getIntExtra(KEY_CANCEL, 0))
+ }
+
+ }
+
+ return START_NOT_STICKY
+
+ }
+
+ private fun sendToot(tootId: Int) {
+
+ // when tootToSend == null, sending has been canceled
+ val tootToSend = tootsToSend[tootId] ?: return
+
+ // when account == null, user has logged out, cancel sending
+ val account = accountManager.getAccountById(tootToSend.accountId)
+
+ if(account == null) {
+ tootsToSend.remove(tootId)
+ return
+ }
+
+ tootToSend.retries++
+
+ val sendCall = mastodonApi.createStatus(
+ "Bearer " + account.accessToken,
+ account.domain,
+ tootToSend.text,
+ tootToSend.inReplyToId,
+ tootToSend.warningText,
+ tootToSend.visibility,
+ tootToSend.sensitive,
+ tootToSend.mediaIds,
+ tootToSend.idempotencyKey
+ )
+
+
+ sendCalls[tootId] = sendCall
+
+ val callback = object: Callback {
+ override fun onResponse(call: Call, response: Response) {
+
+ tootsToSend.remove(tootId)
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ if (response.isSuccessful) {
+
+ val intent = Intent(TimelineReceiver.Types.STATUS_COMPOSED)
+ LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
+
+ // If the status was loaded from a draft, delete the draft and associated media files.
+ if(tootToSend.savedTootUid != 0) {
+ saveTootHelper.deleteDraft(tootToSend.savedTootUid)
+ }
+
+ if (tootsToSend.isEmpty()) {
+ ServiceCompat.stopForeground(this@SendTootService, ServiceCompat.STOP_FOREGROUND_REMOVE)
+ stopSelf()
+ }
+
+ notificationManager.cancel(tootId)
+
+ } else {
+ // the server refused to accept the toot, save toot & show error message
+ saveTootToDrafts(tootToSend)
+
+ val builder = NotificationCompat.Builder(this@SendTootService, CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_notify)
+ .setContentTitle(getString(R.string.send_toot_notification_error_title))
+ .setContentText(getString(R.string.send_toot_notification_saved_content))
+ .setColor(ContextCompat.getColor(this@SendTootService, R.color.primary))
+
+ notificationManager.notify(tootId, builder.build())
+
+ if (tootsToSend.isEmpty()) {
+ ServiceCompat.stopForeground(this@SendTootService, ServiceCompat.STOP_FOREGROUND_DETACH)
+ stopSelf()
+ }
+
+ }
+ }
+
+ override fun onFailure(call: Call, t: Throwable) {
+ var backoff = 1000L*tootToSend.retries
+ if (backoff > MAX_RETRY_INTERVAL) {
+ backoff = MAX_RETRY_INTERVAL
+ }
+
+ timer.schedule(object : TimerTask() {
+ override fun run() {
+ sendToot(tootId)
+ }
+ }, backoff)
+ }
+ }
+
+ sendCall.enqueue(callback)
+
+ }
+
+ private fun cancelSending(tootId: Int) {
+ val tootToCancel = tootsToSend.remove(tootId)
+ if(tootToCancel != null) {
+ val sendCall = sendCalls.remove(tootId)
+ sendCall?.cancel()
+
+ saveTootToDrafts(tootToCancel)
+
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ val builder = NotificationCompat.Builder(this@SendTootService, CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_notify)
+ .setContentTitle(getString(R.string.send_toot_notification_cancel_title))
+ .setContentText(getString(R.string.send_toot_notification_saved_content))
+ .setColor(ContextCompat.getColor(this@SendTootService, R.color.primary))
+
+ notificationManager.notify(tootId, builder.build())
+
+ timer.schedule(object : TimerTask() {
+ override fun run() {
+ notificationManager.cancel(tootId)
+ }
+ }, 5000)
+
+ if (tootsToSend.isEmpty()) {
+ ServiceCompat.stopForeground(this@SendTootService, ServiceCompat.STOP_FOREGROUND_DETACH)
+ stopSelf()
+ }
+
+ }
+ }
+
+ private fun saveTootToDrafts(toot: TootToSend) {
+
+ saveTootHelper.saveToot(toot.text,
+ toot.warningText,
+ toot.savedJsonUrls,
+ toot.mediaUris,
+ toot.savedTootUid,
+ toot.inReplyToId,
+ toot.replyingStatusContent,
+ toot.replyingStatusAuthorUsername,
+ Status.Visibility.byString(toot.visibility))
+ }
+
+ private fun cancelSendingIntent(tootId: Int): PendingIntent {
+
+ val intent = Intent(this, SendTootService::class.java)
+
+ intent.putExtra(KEY_CANCEL, tootId)
+
+ return PendingIntent.getService(this, tootId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+
+ companion object {
+
+ private const val KEY_TOOT = "toot"
+ private const val KEY_CANCEL = "cancel_id"
+ private const val CHANNEL_ID = "send_toots"
+
+ private const val MAX_RETRY_INTERVAL = 60*1000L // 1 minute
+
+ private var notificationId = -1 // use negative ids to not clash with other notis
+
+ @JvmStatic
+ fun sendTootIntent(context: Context,
+ text: String,
+ warningText: String,
+ visibility: Status.Visibility,
+ sensitive: Boolean,
+ mediaIds: List,
+ mediaUris: List,
+ inReplyToId: String?,
+ replyingStatusContent: String?,
+ replyingStatusAuthorUsername: String?,
+ savedJsonUrls: String?,
+ account: AccountEntity,
+ savedTootUid: Int
+ ): Intent {
+ val intent = Intent(context, SendTootService::class.java)
+
+ val idempotencyKey = StringUtils.randomAlphanumericString(16)
+
+ val tootToSend = TootToSend(text,
+ warningText,
+ visibility.serverString(),
+ sensitive,
+ mediaIds,
+ mediaUris,
+ inReplyToId,
+ replyingStatusContent,
+ replyingStatusAuthorUsername,
+ savedJsonUrls,
+ account.id,
+ savedTootUid,
+ idempotencyKey,
+ 0)
+
+ intent.putExtra(KEY_TOOT, tootToSend)
+
+ return intent
+ }
+
+ }
+}
+
+@Parcelize
+data class TootToSend(val text: String,
+ val warningText: String,
+ val visibility: String,
+ val sensitive: Boolean,
+ val mediaIds: List,
+ val mediaUris: List,
+ val inReplyToId: String?,
+ val replyingStatusContent: String?,
+ val replyingStatusAuthorUsername: String?,
+ val savedJsonUrls: String?,
+ val accountId: Long,
+ val savedTootUid: Int,
+ val idempotencyKey: String,
+ var retries: Int): Parcelable
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.java
index 03208429f..5c33991bd 100644
--- a/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.java
+++ b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.java
@@ -28,7 +28,7 @@ import android.text.SpannedString;
import android.text.style.ReplacementSpan;
import android.widget.TextView;
-import com.keylesspalace.tusky.entity.Status;
+import com.keylesspalace.tusky.entity.Emoji;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
@@ -46,12 +46,12 @@ public class CustomEmojiHelper {
* @param textView a reference to the textView the emojis will be shown in
* @return the text with the shortcodes replaced by EmojiSpans
*/
- public static Spanned emojifyText(Spanned text, List emojis, final TextView textView) {
+ public static Spanned emojifyText(Spanned text, List emojis, final TextView textView) {
if (!emojis.isEmpty()) {
SpannableStringBuilder builder = new SpannableStringBuilder(text);
- for (Status.Emoji emoji : emojis) {
+ for (Emoji emoji : emojis) {
CharSequence pattern = new StringBuilder(":").append(emoji.getShortcode()).append(':');
Matcher matcher = Pattern.compile(pattern.toString()).matcher(text);
while (matcher.find()) {
@@ -71,7 +71,7 @@ public class CustomEmojiHelper {
return text;
}
- public static Spanned emojifyString(String string, List emojis, final TextView textView) {
+ public static Spanned emojifyString(String string, List emojis, final TextView textView) {
return emojifyText(new SpannedString(string), emojis, textView);
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/IOUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/IOUtils.java
index 2b17eeeb1..36321f6f2 100644
--- a/app/src/main/java/com/keylesspalace/tusky/util/IOUtils.java
+++ b/app/src/main/java/com/keylesspalace/tusky/util/IOUtils.java
@@ -15,14 +15,22 @@
package com.keylesspalace.tusky.util;
+import android.content.ContentResolver;
+import android.net.Uri;
import android.support.annotation.Nullable;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.OutputStream;
public class IOUtils {
- public static void closeQuietly(@Nullable InputStream stream) {
+
+ private static final int DEFAULT_BLOCKSIZE = 16384;
+
+ public static void closeQuietly(@Nullable Closeable stream) {
try {
if (stream != null) {
stream.close();
@@ -32,13 +40,32 @@ public class IOUtils {
}
}
- public static void closeQuietly(@Nullable OutputStream stream) {
+ public static boolean copyToFile(ContentResolver contentResolver, Uri uri, File file) {
+ InputStream from;
+ FileOutputStream to;
try {
- if (stream != null) {
- stream.close();
+ from = contentResolver.openInputStream(uri);
+ to = new FileOutputStream(file);
+ } catch (FileNotFoundException e) {
+ return false;
+ }
+ if (from == null) {
+ return false;
+ }
+ byte[] chunk = new byte[DEFAULT_BLOCKSIZE];
+ try {
+ while (true) {
+ int bytes = from.read(chunk, 0, chunk.length);
+ if (bytes < 0) {
+ break;
+ }
+ to.write(chunk, 0, bytes);
}
} catch (IOException e) {
- // intentionally unhandled
+ return false;
}
+ closeQuietly(from);
+ closeQuietly(to);
+ return true;
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java
new file mode 100644
index 000000000..87e12da0f
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java
@@ -0,0 +1,197 @@
+package com.keylesspalace.tusky.util;
+
+import android.annotation.SuppressLint;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.content.FileProvider;
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.keylesspalace.tusky.BuildConfig;
+import com.keylesspalace.tusky.db.TootDao;
+import com.keylesspalace.tusky.db.TootEntity;
+import com.keylesspalace.tusky.entity.Status;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+public final class SaveTootHelper {
+
+ private static final String TAG = "SaveTootHelper";
+
+ private TootDao tootDao;
+ private Context context;
+
+ public SaveTootHelper(@NonNull TootDao tootDao, @NonNull Context context) {
+ this.tootDao = tootDao;
+ this.context = context;
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ public boolean saveToot(@NonNull String content,
+ @NonNull String contentWarning,
+ @Nullable String savedJsonUrls,
+ @NonNull List mediaUris,
+ int savedTootUid,
+ @Nullable String inReplyToId,
+ @Nullable String replyingStatusContent,
+ @Nullable String replyingStatusAuthorUsername,
+ @NonNull Status.Visibility statusVisibility) {
+
+ if (TextUtils.isEmpty(content) && mediaUris.isEmpty()) {
+ return false;
+ }
+
+ // Get any existing file's URIs.
+ ArrayList existingUris = null;
+ if (!TextUtils.isEmpty(savedJsonUrls)) {
+ existingUris = new Gson().fromJson(savedJsonUrls,
+ new TypeToken>() {
+ }.getType());
+ }
+
+ String mediaUrlsSerialized = null;
+ if (!ListUtils.isEmpty(mediaUris)) {
+ List savedList = saveMedia(mediaUris, existingUris);
+ if (!ListUtils.isEmpty(savedList)) {
+ mediaUrlsSerialized = new Gson().toJson(savedList);
+ if (!ListUtils.isEmpty(existingUris)) {
+ deleteMedia(setDifference(existingUris, savedList));
+ }
+ } else {
+ return false;
+ }
+ } else if (!ListUtils.isEmpty(existingUris)) {
+ /* If there were URIs in the previous draft, but they've now been removed, those files
+ * can be deleted. */
+ deleteMedia(existingUris);
+ }
+ final TootEntity toot = new TootEntity(savedTootUid, content, mediaUrlsSerialized, contentWarning,
+ inReplyToId,
+ replyingStatusContent,
+ replyingStatusAuthorUsername,
+ statusVisibility);
+
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ tootDao.insertOrReplace(toot);
+ return null;
+ }
+ }.execute();
+ return true;
+ }
+
+ public void deleteDraft(int tootId) {
+ TootEntity item = tootDao.find(tootId);
+ if(item != null) {
+ deleteDraft(item);
+ }
+ }
+
+ public void deleteDraft(@NonNull TootEntity item){
+ // Delete any media files associated with the status.
+ ArrayList uris = new Gson().fromJson(item.getUrls(),
+ new TypeToken>() {}.getType());
+ if (uris != null) {
+ for (String uriString : uris) {
+ Uri uri = Uri.parse(uriString);
+ if (context.getContentResolver().delete(uri, null, null) == 0) {
+ Log.e(TAG, String.format("Did not delete file %s.", uriString));
+ }
+ }
+ }
+ // update DB
+ tootDao.delete(item.getUid());
+ }
+
+ @Nullable
+ private List saveMedia(@NonNull List mediaUris,
+ @Nullable List existingUris) {
+
+ File directory = context.getExternalFilesDir("Tusky");
+
+ if (directory == null || !(directory.exists())) {
+ Log.e(TAG, "Error obtaining directory to save media.");
+ return null;
+ }
+
+ ContentResolver contentResolver = context.getContentResolver();
+ ArrayList filesSoFar = new ArrayList<>();
+ ArrayList results = new ArrayList<>();
+ for (String mediaUri : mediaUris) {
+ /* If the media was already saved in a previous draft, there's no need to save another
+ * copy, just add the existing URI to the results. */
+ if (existingUris != null) {
+ int index = existingUris.indexOf(mediaUri);
+ if (index != -1) {
+ results.add(mediaUri);
+ continue;
+ }
+ }
+ // Otherwise, save the media.
+
+ Uri uri = Uri.parse(mediaUri);
+
+ String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
+
+ String mimeType = contentResolver.getType(uri);
+ MimeTypeMap map = MimeTypeMap.getSingleton();
+ String fileExtension = map.getExtensionFromMimeType(mimeType);
+ String filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension);
+ File file = new File(directory, filename);
+ filesSoFar.add(file);
+ boolean copied = IOUtils.copyToFile(contentResolver, uri, file);
+ if (!copied) {
+ /* If any media files were created in prior iterations, delete those before
+ * returning. */
+ for (File earlierFile : filesSoFar) {
+ boolean deleted = earlierFile.delete();
+ if (!deleted) {
+ Log.i(TAG, "Could not delete the file " + earlierFile.toString());
+ }
+ }
+ return null;
+ }
+ Uri resultUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID+".fileprovider", file);
+ results.add(resultUri.toString());
+ }
+ return results;
+ }
+
+ private void deleteMedia(List mediaUris) {
+ for (String uriString : mediaUris) {
+ Uri uri = Uri.parse(uriString);
+ if (context.getContentResolver().delete(uri, null, null) == 0) {
+ Log.e(TAG, String.format("Did not delete file %s.", uriString));
+ }
+ }
+ }
+
+ /**
+ * A∖B={x∈A|x∉B}
+ *
+ * @return all elements of set A that are not in set B.
+ */
+ private static List setDifference(List a, List b) {
+ List c = new ArrayList<>();
+ for (String s : a) {
+ if (!b.contains(s)) {
+ c.add(s);
+ }
+ }
+ return c;
+ }
+
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/view/ComposeOptionsView.kt b/app/src/main/java/com/keylesspalace/tusky/view/ComposeOptionsView.kt
new file mode 100644
index 000000000..becbb8317
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/view/ComposeOptionsView.kt
@@ -0,0 +1,80 @@
+/* Copyright 2018 Conny Duck
+ *
+ * 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 . */
+
+package com.keylesspalace.tusky.view
+
+import android.content.Context
+import android.os.Build
+import android.util.AttributeSet
+import android.widget.LinearLayout
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.entity.Status
+import kotlinx.android.synthetic.main.view_compose_options.view.*
+
+
+class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : LinearLayout(context, attrs, defStyleAttr) {
+
+ var listener: ComposeOptionsListener? = null
+
+ init {
+ inflate(context, R.layout.view_compose_options, this)
+
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ publicRadioButton.setButtonDrawable(R.drawable.ic_public_24dp)
+ unlistedRadioButton.setButtonDrawable(R.drawable.ic_lock_open_24dp)
+ privateRadioButton.setButtonDrawable(R.drawable.ic_lock_outline_24dp)
+ directRadioButton.setButtonDrawable(R.drawable.ic_email_24dp)
+ }
+
+ visibilityRadioGroup.setOnCheckedChangeListener({ _, checkedId ->
+ val visibility = when (checkedId) {
+ R.id.publicRadioButton ->
+ Status.Visibility.PUBLIC
+ R.id.unlistedRadioButton ->
+ Status.Visibility.UNLISTED
+ R.id.privateRadioButton ->
+ Status.Visibility.PRIVATE
+ R.id.directRadioButton ->
+ Status.Visibility.DIRECT
+ else ->
+ Status.Visibility.PUBLIC
+ }
+ listener?.onVisibilityChanged(visibility)
+ })
+ }
+
+ fun setStatusVisibility(visibility: Status.Visibility) {
+ val selectedButton = when (visibility) {
+ Status.Visibility.PUBLIC ->
+ R.id.publicRadioButton
+ Status.Visibility.UNLISTED ->
+ R.id.unlistedRadioButton
+ Status.Visibility.PRIVATE ->
+ R.id.privateRadioButton
+ Status.Visibility.DIRECT ->
+ R.id.directRadioButton
+ else ->
+ R.id.directRadioButton
+
+ }
+
+ visibilityRadioGroup.check(selectedButton)
+ }
+
+}
+
+interface ComposeOptionsListener {
+ fun onVisibilityChanged(visibility: Status.Visibility)
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/view/EditTextTyped.java b/app/src/main/java/com/keylesspalace/tusky/view/EditTextTyped.java
deleted file mode 100644
index 8de38513e..000000000
--- a/app/src/main/java/com/keylesspalace/tusky/view/EditTextTyped.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/* 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 . */
-
-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.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 AppCompatMultiAutoCompleteTextView {
-
- private InputConnectionCompat.OnCommitContentListener onCommitContentListener;
- private String[] mimeTypes;
-
- public EditTextTyped(Context context) {
- super(context);
- }
-
- public EditTextTyped(Context context, AttributeSet attributeSet) {
- super(context, attributeSet);
- }
-
- public void setMimeTypes(String[] types,
- InputConnectionCompat.OnCommitContentListener listener) {
- mimeTypes = types;
- onCommitContentListener = listener;
- }
-
- @Override
- public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
- InputConnection connection = super.onCreateInputConnection(editorInfo);
- if (onCommitContentListener != null) {
- Assert.expect(mimeTypes != null);
- EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes);
- return InputConnectionCompat.createWrapper(connection, editorInfo,
- onCommitContentListener);
- } else {
- return connection;
- }
- }
-}
diff --git a/app/src/main/java/com/keylesspalace/tusky/view/EditTextTyped.kt b/app/src/main/java/com/keylesspalace/tusky/view/EditTextTyped.kt
new file mode 100644
index 000000000..fb837b153
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/view/EditTextTyped.kt
@@ -0,0 +1,53 @@
+/* Copyright 2018 Conny Duck
+ *
+ * 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 . */
+
+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.AppCompatMultiAutoCompleteTextView
+import android.text.InputType
+import android.util.AttributeSet
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.InputConnection
+
+class EditTextTyped @JvmOverloads constructor(context: Context,
+ attributeSet: AttributeSet? = null)
+ : AppCompatMultiAutoCompleteTextView(context, attributeSet) {
+
+ private var onCommitContentListener: InputConnectionCompat.OnCommitContentListener? = null
+
+ init {
+ //fix a bug with autocomplete and some keyboards
+ val newInputType = inputType and (inputType xor InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE)
+ inputType = newInputType
+ }
+
+ fun setOnCommitContentListener(listener: InputConnectionCompat.OnCommitContentListener) {
+ onCommitContentListener = listener
+ }
+
+ override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
+ val connection = super.onCreateInputConnection(editorInfo)
+ return if (onCommitContentListener != null) {
+ EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
+ InputConnectionCompat.createWrapper(connection, editorInfo,
+ onCommitContentListener!!)
+ } else {
+ connection
+ }
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/view/TootButton.kt b/app/src/main/java/com/keylesspalace/tusky/view/TootButton.kt
new file mode 100644
index 000000000..088fc051e
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/view/TootButton.kt
@@ -0,0 +1,76 @@
+/* Copyright 2018 Conny Duck
+ *
+ * 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 . */
+
+package com.keylesspalace.tusky.view
+
+import android.content.Context
+import android.graphics.Color
+import android.support.v7.widget.AppCompatButton
+import android.util.AttributeSet
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.entity.Status
+import com.mikepenz.google_material_typeface_library.GoogleMaterial
+import com.mikepenz.iconics.IconicsDrawable
+
+class TootButton
+@JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : AppCompatButton(context, attrs, defStyleAttr) {
+
+ private val smallStyle: Boolean = context.resources.getBoolean(R.bool.show_small_toot_button)
+
+ init {
+ if(smallStyle) {
+ setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_send_24dp, 0, 0, 0)
+ } else {
+ compoundDrawablePadding = context.resources.getDimensionPixelSize(R.dimen.toot_button_drawable_padding)
+ setText(R.string.action_send)
+ }
+ }
+
+ fun setStatusVisibility(visibility: Status.Visibility) {
+ if(!smallStyle) {
+
+ when (visibility) {
+ Status.Visibility.PUBLIC -> {
+ setText(R.string.action_send_public)
+ setCompoundDrawables(null, null, null, null)
+ }
+ Status.Visibility.UNLISTED -> {
+ setText(R.string.action_send)
+ setCompoundDrawables(null, null, null, null)
+ }
+ Status.Visibility.PRIVATE,
+ Status.Visibility.DIRECT -> {
+ addLock()
+ }
+ else -> {
+ setCompoundDrawables(null, null, null, null)
+ }
+ }
+ }
+
+ }
+
+ private fun addLock() {
+ setText(R.string.action_send)
+ val lock = IconicsDrawable(context, GoogleMaterial.Icon.gmd_lock).sizeDp(18).color(Color.WHITE)
+ setCompoundDrawablesWithIntrinsicBounds(lock, null, null, null)
+ }
+
+}
+
diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java
index 46c62f7b6..639677bcd 100644
--- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java
+++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java
@@ -20,6 +20,7 @@ import android.text.Spanned;
import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Card;
+import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Status;
import java.util.Collections;
@@ -68,7 +69,7 @@ public abstract class StatusViewData {
private final String senderId;
private final boolean rebloggingEnabled;
private final Status.Application application;
- private final List emojis;
+ private final List emojis;
@Nullable
private final Card card;
@@ -78,7 +79,7 @@ public abstract class StatusViewData {
boolean isShowingContent, String userFullName, String nickname, String avatar,
Date createdAt, int reblogsCount, int favouritesCount, @Nullable String inReplyToId,
@Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled,
- Status.Application application, List emojis, @Nullable Card card) {
+ Status.Application application, List emojis, @Nullable Card card) {
this.id = id;
this.content = content;
this.reblogged = reblogged;
@@ -203,7 +204,7 @@ public abstract class StatusViewData {
return application;
}
- public List getEmojis() {
+ public List getEmojis() {
return emojis;
}
@@ -250,7 +251,7 @@ public abstract class StatusViewData {
private String senderId;
private boolean rebloggingEnabled;
private Status.Application application;
- private List emojis;
+ private List emojis;
private Card card;
public Builder() {
@@ -399,7 +400,7 @@ public abstract class StatusViewData {
return this;
}
- public Builder setEmojis(List emojis) {
+ public Builder setEmojis(List emojis) {
this.emojis = emojis;
return this;
}
diff --git a/app/src/main/res/color/compound_button_color_dark.xml b/app/src/main/res/color/compound_button_color_dark.xml
new file mode 100644
index 000000000..c3a3d0710
--- /dev/null
+++ b/app/src/main/res/color/compound_button_color_dark.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/color/compound_button_color_light.xml b/app/src/main/res/color/compound_button_color_light.xml
new file mode 100644
index 000000000..8c113f771
--- /dev/null
+++ b/app/src/main/res/color/compound_button_color_light.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/compose_button_colors.xml b/app/src/main/res/drawable/compose_button_colors.xml
deleted file mode 100644
index 2487c7a34..000000000
--- a/app/src/main/res/drawable/compose_button_colors.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
- -
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/favourite_inactive_dark.xml b/app/src/main/res/drawable/favourite_inactive_dark.xml
index 803131bc1..cc1dc796a 100644
--- a/app/src/main/res/drawable/favourite_inactive_dark.xml
+++ b/app/src/main/res/drawable/favourite_inactive_dark.xml
@@ -4,6 +4,6 @@ android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
diff --git a/app/src/main/res/drawable/favourite_inactive_light.xml b/app/src/main/res/drawable/favourite_inactive_light.xml
index 06ccf499e..0683a47bd 100644
--- a/app/src/main/res/drawable/favourite_inactive_light.xml
+++ b/app/src/main/res/drawable/favourite_inactive_light.xml
@@ -4,7 +4,7 @@ android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
diff --git a/app/src/main/res/drawable/ic_email_24dp.xml b/app/src/main/res/drawable/ic_email_24dp.xml
index a050d6f8f..1bcee1be0 100644
--- a/app/src/main/res/drawable/ic_email_24dp.xml
+++ b/app/src/main/res/drawable/ic_email_24dp.xml
@@ -1,9 +1,7 @@
-
-
+ android:width="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_emoji_24dp.xml b/app/src/main/res/drawable/ic_emoji_24dp.xml
new file mode 100644
index 000000000..5a73c89d5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_emoji_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_remove_red_eye_black_24dp.xml b/app/src/main/res/drawable/ic_eye_24dp.xml
similarity index 66%
rename from app/src/main/res/drawable/ic_remove_red_eye_black_24dp.xml
rename to app/src/main/res/drawable/ic_eye_24dp.xml
index e58240d25..408cb26f6 100644
--- a/app/src/main/res/drawable/ic_remove_red_eye_black_24dp.xml
+++ b/app/src/main/res/drawable/ic_eye_24dp.xml
@@ -1,9 +1,9 @@
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ android:fillColor="#fff"
+ android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z" />
diff --git a/app/src/main/res/drawable/ic_hide_media_24dp.xml b/app/src/main/res/drawable/ic_hide_media_24dp.xml
index 8b64ae1a6..ed10b8cc9 100644
--- a/app/src/main/res/drawable/ic_hide_media_24dp.xml
+++ b/app/src/main/res/drawable/ic_hide_media_24dp.xml
@@ -1,7 +1,9 @@
-
-
-
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_save_24dp.xml b/app/src/main/res/drawable/ic_save_24dp.xml
deleted file mode 100644
index 7e982fe5c..000000000
--- a/app/src/main/res/drawable/ic_save_24dp.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/reblog_inactive_dark.xml b/app/src/main/res/drawable/reblog_inactive_dark.xml
index 74d6b8fe5..ca335ebbf 100644
--- a/app/src/main/res/drawable/reblog_inactive_dark.xml
+++ b/app/src/main/res/drawable/reblog_inactive_dark.xml
@@ -4,6 +4,6 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
diff --git a/app/src/main/res/drawable/reblog_inactive_light.xml b/app/src/main/res/drawable/reblog_inactive_light.xml
index 7b6497917..cfeb3ce67 100644
--- a/app/src/main/res/drawable/reblog_inactive_light.xml
+++ b/app/src/main/res/drawable/reblog_inactive_light.xml
@@ -4,6 +4,6 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
diff --git a/app/src/main/res/drawable/send_private.xml b/app/src/main/res/drawable/send_private.xml
deleted file mode 100644
index 2b5117ca9..000000000
--- a/app/src/main/res/drawable/send_private.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/layout/activity_compose.xml b/app/src/main/res/layout/activity_compose.xml
index 674542684..debf87a4f 100644
--- a/app/src/main/res/layout/activity_compose.xml
+++ b/app/src/main/res/layout/activity_compose.xml
@@ -6,115 +6,122 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+ android:layout_marginBottom="52dp"
+ android:layout_marginTop="?attr/actionBarSize">
-
+
+
+ android:paddingTop="4dp"
+ android:textSize="?attr/status_text_small"
+ android:visibility="gone"
+ tools:text="Post content which may be preeettyy long, so please, make sure there's enough room for everything, okay? Not kidding. I wish Eugen answered me more often, sigh."
+ tools:visibility="visible" />
-
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
-
+
-
+
+
+
+ android:scrollbars="none">
+ android:orientation="horizontal"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp">
@@ -122,104 +129,162 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_compose_options.xml b/app/src/main/res/layout/fragment_compose_options.xml
deleted file mode 100644
index f1cdc1328..000000000
--- a/app/src/main/res/layout/fragment_compose_options.xml
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_emoji_button.xml b/app/src/main/res/layout/item_emoji_button.xml
new file mode 100644
index 000000000..618a957b3
--- /dev/null
+++ b/app/src/main/res/layout/item_emoji_button.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_status.xml b/app/src/main/res/layout/item_status.xml
index bfe1a3a0e..dd71db812 100644
--- a/app/src/main/res/layout/item_status.xml
+++ b/app/src/main/res/layout/item_status.xml
@@ -244,7 +244,7 @@
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="@+id/status_media_preview_container"
app:layout_constraintTop_toTopOf="@+id/status_media_preview_container"
- app:srcCompat="@drawable/ic_remove_red_eye_black_24dp" />
+ app:srcCompat="@drawable/ic_eye_24dp" />
+ app:srcCompat="@drawable/ic_eye_24dp" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 6d9464679..d4419149a 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -71,7 +71,6 @@
تبويق
بوّق
إعادة المحاولة
- اخفي النص وراء تحذير
إغلاق
الملف الشخصي
التفضيلات
@@ -81,14 +80,13 @@
طلبات المتابعة
وسائط
إفتح في متصفح
- إضافة وسائط
+ إضافة وسائط
أخذ صورة
شارك
أكتم
إلغاء الكتم
أذكر
إخفاء الوسائط
- خيارات
إفتح الدرج
إحفظ
تعديل الملف الشخصي
@@ -105,7 +103,6 @@
شارك رابط التبويق على ...
شارك التبويق على …
- بَوِّق
تم الإرسال !
تم فك الحجب عن الحساب
تم فك الكتم عن الحساب
@@ -131,7 +128,6 @@
تنزيل
طلب المتابعة معلق : في إنتظار الرد
هل تود إلغاء متابعة هذا الحساب ؟
- تعذرت عملية إرسال هذا المنشور. إنّ المنشور الذي تود الرد عليه غير متوفر. هل تود حذف نص الرد ؟
عمومي : ينشر على الخيوط العمومية
غير مدرج : لا يُعرَض على الخيوط العمومية
@@ -244,7 +240,6 @@
طلب متابعة
ليس هناك محتوى
- تم حفظ التبويق !
في %dy
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index 5c057077e..8295a78bd 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -9,7 +9,7 @@
L\'autorització s\'ha denegat.
L\'obtenció del testimoni d\'inici de sessió ha fallat.
L\'estat és massa llarg!
- El fitxer ha de ser inferior a 4MB.
+ El fitxer ha de ser inferior a 8MB.
Aquest tipus de fitxer no es pot pujar.
Aquest tipus de fitxer no es pot obrir.
Cal permís de lectura del mitjà.
@@ -69,7 +69,6 @@
TOOT
TOOT!
Torna a intentar-ho
- Amaga el text amb un avís
Tanca
Perfil
Preferències
@@ -79,14 +78,13 @@
Peticions de seguiment
Multimèdia
Obre al navegador
- Afegeix multimètida
+ Afegeix multimètida
Fes una foto
Comparteix
Silencia
Deixa de silenciar
Menciona
Amaga el multimèdia
- Opcions
Open drawer
Desa
Edita el perfil
@@ -103,7 +101,6 @@
Comparteix l\'URL del toot a...
Comparteix el toot a...
- Toot!
Enviat!
Usuari desblocat
Usuari sense silenciar
@@ -137,7 +134,6 @@
Baixa
Follow request pending: awaiting their response
Vols deixar de seguir aquest compte?
- Couldn\'t post this status. The status you\'re replying to might not be available. Remove reply info?
Pública: és visible a la cronologia pública
Sense llistar: no és visible a les cronologies públiques
@@ -236,7 +232,6 @@
Follow requested
no hi ha cap contingut
- S\'ha desat el toot!
en %d anys
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 9af04bf38..bde32431c 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -9,7 +9,7 @@
Autorisierung fehlgeschlagen.
Es konnte kein Login-Token abgerufen werden.
Der Beitrag ist zu lang!
- Die Datei muss kleiner als 4MB sein.
+ Die Datei muss kleiner als 8MB sein.
Dieser Dateityp darf nicht hochgeladen werden.
Die Datei konnte nicht geöffnet werden.
Eine Leseberechtigung wird für das Hochladen der Mediendatei benötigt.
@@ -63,7 +63,6 @@
TRÖT
TRÖT!
Erneut versuchen
- Verstecke Text hinter Warnung
Schließen
Profil
Einstellungen
@@ -71,13 +70,12 @@
Blockierte Accounts
Medien
Im Browser öffnen
- Füge Medien hinzu
+ Füge Medien hinzu
Foto machen
Teilen
Stummschalten
Lautschalten
Erwähnen
- Einstellungen
Drawer öffnen
Suche
Entwürfe
@@ -87,7 +85,6 @@
Beitragslink teilen
Beitragsinhalt teilen
- Teilen!
Gesendet!
Welche Instanz?
@@ -199,7 +196,6 @@
Tuskys Profil
- Beitrag gespeichert
Keine Ergebnisse
Bilder
Video
@@ -232,7 +228,6 @@
%s Folgt
%s Beiträge
mehr laden
- Fehler beim Senden des Status. Der Status, auf den du antwortest, ist vielleicht nicht mehr verfügbar. Als normale Erwähnung weiter bearbeiten?
Beitragssichtbarkeit
Beiträge
Medien versteckt
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index d1bf82f7a..5eef42c42 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -9,7 +9,7 @@
Authentification refusée.
Impossible de récupérer le jeton d’authentification.
Votre pouet est trop long !
- Le fichier doit peser moins de 4 Mo.
+ Le fichier doit peser moins de 8 Mo.
Ce type de fichier n’est pas accepté.
Le fichier ne peut pas être ouvert.
Permission requise pour lire ce média.
@@ -66,7 +66,6 @@
POUET
POUET !
Réessayer
- Masquer le texte par une mise en garde.
Fermer
Profil
Préférences
@@ -74,14 +73,12 @@
Utilisateurs bloqués
Média
Ouvrir dans votre navigateur
- Ajouter un média
+ Ajouter un média
Prendre une photo
Partager
Rendre muet
Redonner la parole
Mention
-
- Options
Ouvrir le menu
Sauvegarder
Modifier le profil
@@ -98,7 +95,6 @@
Partager le pouet avec…
Téléchargement de %1$s
- Pouet !
Envoyé !
Quelle instance ?
@@ -244,9 +240,6 @@
Images
Videos
aucun contenu
- Pouet enregistré!
-
- Impssible de poster ce status. Le status auquel vous répondez peut ne plus être disponible. Supprimer la réponse?
Êtes vous certain de vouloir déconnecter le compte %1$s?
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index 21e84bcb0..05b672e11 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -9,7 +9,7 @@
Engedélyezés letiltva.
Bejelentkezési token megszerzése sikertelen.
Túl hosszú a tülkölés!
- A fájl kisebb kell legyen mint 4MB.
+ A fájl kisebb kell legyen mint 8MB.
Fájl feltöltése sikertelen.
Fájl megnyitása sikertelen.
Média olvasási engedély szükséges.
@@ -66,7 +66,6 @@
TÜLK
TÜLK!
Próbálja újra
- Szöveg figyelmeztetés mögé helyezése
Bezár
Profil
Preferenciák
@@ -76,14 +75,13 @@
Követési kérések
Média
Megnyitás böngészőben
- Média hozzácsatolása
+ Média hozzácsatolása
Kép készítése
Megoszt
Némítás
Kinémítás
Megemlítés
Média elrejtése
- Beállítások
Drawer megnyitása
Mentés
Profil szerkesztése
@@ -100,7 +98,6 @@
Tülk URL megosztása…
Tülk megosztása…
- Tülk!
Elküldve!
Felhasználó deblokkolva
Felhasználó kinémítva
@@ -201,7 +198,6 @@
Követés kérelmezve
nincs tartalom
- Tülk mentve!
Követ téged
Mindig mutassa a NSFW tartalmat
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index c1509b45c..77d1b23ae 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -71,7 +71,6 @@
トゥート
トゥート!
再試行
- テキストを注意書きで隠す
閉じる
プロフィール
設定
@@ -81,14 +80,13 @@
フォローリクエスト
メディア
ブラウザで開く
- メディアを追加
+ メディアを追加
写真を撮る
共有
ミュート
ミュート解除
返信
メディアを隠す
- オプション
メニューを開く
保存
プロフィールを編集
@@ -105,7 +103,6 @@
トゥートのURLを共有…
トゥートを共有…
- トゥート!
送信しました!
ブロックを解除しました
ミュートを解除しました
@@ -252,7 +249,6 @@
フォローリクエスト中
下書きはありません
- 下書きに保存しました!
%d年後
diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml
index 798567415..a1e88bf0c 100644
--- a/app/src/main/res/values-night/styles.xml
+++ b/app/src/main/res/values-night/styles.xml
@@ -43,13 +43,9 @@
- @color/account_toolbar_icon_collapsed_dark
- @style/AppTheme.Account.ToolbarPopupTheme.Dark
- @color/toolbar_icon_dark
- - @color/compose_media_button_dark
- @color/compose_media_button_disabled_dark
- @color/color_accent_dark
- @drawable/border_background_dark
- - @color/image_button_dark
- - @color/color_accent_dark
- - @color/image_button_dark
- @color/compose_reply_content_background_dark
- @color/color_background_dark
@@ -70,10 +66,13 @@
- @drawable/ic_play_indicator_dark
+ - @color/compound_button_color_dark
+