|
|
|
|
@@ -2,12 +2,19 @@ package org.joinmastodon.android.fragments;
|
|
|
|
|
|
|
|
|
|
import android.annotation.SuppressLint;
|
|
|
|
|
import android.app.Activity;
|
|
|
|
|
import android.app.ProgressDialog;
|
|
|
|
|
import android.content.ClipData;
|
|
|
|
|
import android.content.Intent;
|
|
|
|
|
import android.content.res.Configuration;
|
|
|
|
|
import android.graphics.Outline;
|
|
|
|
|
import android.icu.text.BreakIterator;
|
|
|
|
|
import android.net.Uri;
|
|
|
|
|
import android.os.Build;
|
|
|
|
|
import android.os.Bundle;
|
|
|
|
|
import android.text.Editable;
|
|
|
|
|
import android.text.TextWatcher;
|
|
|
|
|
import android.util.Log;
|
|
|
|
|
import android.view.Gravity;
|
|
|
|
|
import android.view.LayoutInflater;
|
|
|
|
|
import android.view.Menu;
|
|
|
|
|
import android.view.MenuInflater;
|
|
|
|
|
@@ -16,9 +23,13 @@ import android.view.View;
|
|
|
|
|
import android.view.ViewGroup;
|
|
|
|
|
import android.view.ViewOutlineProvider;
|
|
|
|
|
import android.view.inputmethod.InputMethodManager;
|
|
|
|
|
import android.widget.Button;
|
|
|
|
|
import android.widget.EditText;
|
|
|
|
|
import android.widget.FrameLayout;
|
|
|
|
|
import android.widget.ImageButton;
|
|
|
|
|
import android.widget.ImageView;
|
|
|
|
|
import android.widget.LinearLayout;
|
|
|
|
|
import android.widget.ProgressBar;
|
|
|
|
|
import android.widget.TextView;
|
|
|
|
|
|
|
|
|
|
import com.twitter.twittertext.Regex;
|
|
|
|
|
@@ -26,31 +37,40 @@ import com.twitter.twittertext.TwitterTextEmojiRegex;
|
|
|
|
|
|
|
|
|
|
import org.joinmastodon.android.E;
|
|
|
|
|
import org.joinmastodon.android.R;
|
|
|
|
|
import org.joinmastodon.android.api.ProgressListener;
|
|
|
|
|
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
|
|
|
|
import org.joinmastodon.android.api.requests.statuses.UploadAttachment;
|
|
|
|
|
import org.joinmastodon.android.api.session.AccountSession;
|
|
|
|
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
|
|
|
|
import org.joinmastodon.android.events.StatusCreatedEvent;
|
|
|
|
|
import org.joinmastodon.android.model.Account;
|
|
|
|
|
import org.joinmastodon.android.model.Attachment;
|
|
|
|
|
import org.joinmastodon.android.model.Emoji;
|
|
|
|
|
import org.joinmastodon.android.model.EmojiCategory;
|
|
|
|
|
import org.joinmastodon.android.model.Status;
|
|
|
|
|
import org.joinmastodon.android.ui.CustomEmojiPopupKeyboard;
|
|
|
|
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
|
|
|
|
import org.joinmastodon.android.ui.PopupKeyboard;
|
|
|
|
|
import org.joinmastodon.android.ui.views.SizeListenerLinearLayout;
|
|
|
|
|
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.UUID;
|
|
|
|
|
import java.util.regex.Pattern;
|
|
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
|
|
import me.grishka.appkit.Nav;
|
|
|
|
|
import me.grishka.appkit.api.Callback;
|
|
|
|
|
import me.grishka.appkit.api.ErrorResponse;
|
|
|
|
|
import me.grishka.appkit.fragments.OnBackPressedListener;
|
|
|
|
|
import me.grishka.appkit.fragments.ToolbarFragment;
|
|
|
|
|
import me.grishka.appkit.imageloader.ViewImageLoader;
|
|
|
|
|
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
|
|
|
|
import me.grishka.appkit.utils.V;
|
|
|
|
|
|
|
|
|
|
public class ComposeFragment extends ToolbarFragment{
|
|
|
|
|
public class ComposeFragment extends ToolbarFragment implements OnBackPressedListener{
|
|
|
|
|
|
|
|
|
|
private static final int MEDIA_RESULT=717;
|
|
|
|
|
|
|
|
|
|
private static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE);
|
|
|
|
|
|
|
|
|
|
@@ -81,10 +101,14 @@ public class ComposeFragment extends ToolbarFragment{
|
|
|
|
|
private EditText mainEditText;
|
|
|
|
|
private TextView charCounter;
|
|
|
|
|
private String accountID;
|
|
|
|
|
private int charCount, charLimit;
|
|
|
|
|
private int charCount, charLimit, trimmedCharCount;
|
|
|
|
|
|
|
|
|
|
private MenuItem publishButton;
|
|
|
|
|
private ImageButton emojiBtn;
|
|
|
|
|
private Button publishButton;
|
|
|
|
|
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn;
|
|
|
|
|
private LinearLayout attachmentsView;
|
|
|
|
|
|
|
|
|
|
private ArrayList<DraftMediaAttachment> queuedAttachments=new ArrayList<>(), failedAttachments=new ArrayList<>(), attachments=new ArrayList<>();
|
|
|
|
|
private DraftMediaAttachment uploadingAttachment;
|
|
|
|
|
|
|
|
|
|
private List<EmojiCategory> customEmojis;
|
|
|
|
|
private CustomEmojiPopupKeyboard emojiKeyboard;
|
|
|
|
|
@@ -127,7 +151,13 @@ public class ComposeFragment extends ToolbarFragment{
|
|
|
|
|
selfAvatar.setOutlineProvider(roundCornersOutline);
|
|
|
|
|
selfAvatar.setClipToOutline(true);
|
|
|
|
|
|
|
|
|
|
mediaBtn=view.findViewById(R.id.btn_media);
|
|
|
|
|
pollBtn=view.findViewById(R.id.btn_poll);
|
|
|
|
|
emojiBtn=view.findViewById(R.id.btn_emoji);
|
|
|
|
|
spoilerBtn=view.findViewById(R.id.btn_spoiler);
|
|
|
|
|
visibilityBtn=view.findViewById(R.id.btn_visibility);
|
|
|
|
|
|
|
|
|
|
mediaBtn.setOnClickListener(v->openFilePicker());
|
|
|
|
|
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
|
|
|
|
|
emojiKeyboard.setOnIconChangedListener(new PopupKeyboard.OnIconChangeListener(){
|
|
|
|
|
@Override
|
|
|
|
|
@@ -138,6 +168,9 @@ public class ComposeFragment extends ToolbarFragment{
|
|
|
|
|
|
|
|
|
|
contentView=(SizeListenerLinearLayout) view;
|
|
|
|
|
contentView.addView(emojiKeyboard.getView());
|
|
|
|
|
emojiKeyboard.getView().setElevation(V.dp(2));
|
|
|
|
|
|
|
|
|
|
attachmentsView=view.findViewById(R.id.attachments);
|
|
|
|
|
|
|
|
|
|
return view;
|
|
|
|
|
}
|
|
|
|
|
@@ -173,35 +206,26 @@ public class ComposeFragment extends ToolbarFragment{
|
|
|
|
|
updateCharCounter(s);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
updateToolbar();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
|
|
|
|
publishButton=menu.add("TOOT!");
|
|
|
|
|
publishButton.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
|
|
|
|
publishButton=new Button(getActivity());
|
|
|
|
|
publishButton.setText(R.string.publish);
|
|
|
|
|
publishButton.setOnClickListener(this::onPublishClick);
|
|
|
|
|
FrameLayout wrap=new FrameLayout(getActivity());
|
|
|
|
|
wrap.addView(publishButton, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP|Gravity.LEFT));
|
|
|
|
|
wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
|
|
|
|
|
wrap.setClipToPadding(false);
|
|
|
|
|
MenuItem item=menu.add(R.string.publish);
|
|
|
|
|
item.setActionView(wrap);
|
|
|
|
|
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
|
|
|
|
updatePublishButtonState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean onOptionsItemSelected(MenuItem item){
|
|
|
|
|
String text=mainEditText.getText().toString();
|
|
|
|
|
CreateStatus.Request req=new CreateStatus.Request();
|
|
|
|
|
req.status=text;
|
|
|
|
|
String uuid=UUID.randomUUID().toString();
|
|
|
|
|
new CreateStatus(req, uuid)
|
|
|
|
|
.setCallback(new Callback<>(){
|
|
|
|
|
@Override
|
|
|
|
|
public void onSuccess(Status result){
|
|
|
|
|
Nav.finish(ComposeFragment.this);
|
|
|
|
|
E.post(new StatusCreatedEvent(result));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onError(ErrorResponse error){
|
|
|
|
|
error.showToast(getActivity());
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.exec(accountID);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -209,6 +233,7 @@ public class ComposeFragment extends ToolbarFragment{
|
|
|
|
|
public void onConfigurationChanged(Configuration newConfig){
|
|
|
|
|
super.onConfigurationChanged(newConfig);
|
|
|
|
|
emojiKeyboard.onConfigurationChanged();
|
|
|
|
|
updateToolbar();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@SuppressLint("NewApi")
|
|
|
|
|
@@ -225,14 +250,200 @@ public class ComposeFragment extends ToolbarFragment{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
charCounter.setText(String.valueOf(charLimit-charCount));
|
|
|
|
|
trimmedCharCount=text.toString().trim().length();
|
|
|
|
|
updatePublishButtonState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void updatePublishButtonState(){
|
|
|
|
|
publishButton.setEnabled(charCount>0 && charCount<=charLimit);
|
|
|
|
|
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && uploadingAttachment==null && failedAttachments.isEmpty() && queuedAttachments.isEmpty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void onCustomEmojiClick(Emoji emoji){
|
|
|
|
|
mainEditText.getText().replace(mainEditText.getSelectionStart(), mainEditText.getSelectionEnd(), ':'+emoji.shortcode+':');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void updateToolbar(){
|
|
|
|
|
getToolbar().setNavigationIcon(R.drawable.ic_fluent_dismiss_24_regular);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void onPublishClick(View v){
|
|
|
|
|
publish();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void publish(){
|
|
|
|
|
String text=mainEditText.getText().toString();
|
|
|
|
|
CreateStatus.Request req=new CreateStatus.Request();
|
|
|
|
|
req.status=text;
|
|
|
|
|
if(!attachments.isEmpty()){
|
|
|
|
|
req.mediaIds=attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList());
|
|
|
|
|
}
|
|
|
|
|
String uuid=UUID.randomUUID().toString();
|
|
|
|
|
ProgressDialog progress=new ProgressDialog(getActivity());
|
|
|
|
|
progress.setMessage(getString(R.string.publishing));
|
|
|
|
|
progress.setCancelable(false);
|
|
|
|
|
progress.show();
|
|
|
|
|
new CreateStatus(req, uuid)
|
|
|
|
|
.setCallback(new Callback<>(){
|
|
|
|
|
@Override
|
|
|
|
|
public void onSuccess(Status result){
|
|
|
|
|
progress.dismiss();
|
|
|
|
|
Nav.finish(ComposeFragment.this);
|
|
|
|
|
E.post(new StatusCreatedEvent(result));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onError(ErrorResponse error){
|
|
|
|
|
progress.dismiss();
|
|
|
|
|
error.showToast(getActivity());
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.exec(accountID);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean hasDraft(){
|
|
|
|
|
return mainEditText.length()>0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean onBackPressed(){
|
|
|
|
|
if(emojiKeyboard.isVisible()){
|
|
|
|
|
emojiKeyboard.hide();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if(hasDraft()){
|
|
|
|
|
confirmDiscardDraftAndFinish();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onToolbarNavigationClick(){
|
|
|
|
|
if(hasDraft()){
|
|
|
|
|
confirmDiscardDraftAndFinish();
|
|
|
|
|
}else{
|
|
|
|
|
super.onToolbarNavigationClick();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void confirmDiscardDraftAndFinish(){
|
|
|
|
|
new M3AlertDialogBuilder(getActivity())
|
|
|
|
|
.setTitle(R.string.discard_draft)
|
|
|
|
|
.setPositiveButton(R.string.discard, (dialog, which)->Nav.finish(this))
|
|
|
|
|
.setNegativeButton(R.string.cancel, null)
|
|
|
|
|
.show();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void openFilePicker(){
|
|
|
|
|
Intent intent=new Intent(Intent.ACTION_GET_CONTENT);
|
|
|
|
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
|
|
|
|
intent.setType("*/*");
|
|
|
|
|
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
|
|
|
|
|
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
|
|
|
|
startActivityForResult(intent, MEDIA_RESULT);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onActivityResult(int requestCode, int resultCode, Intent data){
|
|
|
|
|
if(requestCode==MEDIA_RESULT && resultCode==Activity.RESULT_OK){
|
|
|
|
|
Uri single=data.getData();
|
|
|
|
|
if(single!=null){
|
|
|
|
|
addMediaAttachment(single);
|
|
|
|
|
}else{
|
|
|
|
|
ClipData clipData=data.getClipData();
|
|
|
|
|
for(int i=0;i<clipData.getItemCount();i++){
|
|
|
|
|
addMediaAttachment(clipData.getItemAt(i).getUri());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void addMediaAttachment(Uri uri){
|
|
|
|
|
View thumb=getActivity().getLayoutInflater().inflate(R.layout.compose_media_thumb, attachmentsView, false);
|
|
|
|
|
ImageView img=thumb.findViewById(R.id.thumb);
|
|
|
|
|
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(uri, V.dp(250), V.dp(250)));
|
|
|
|
|
attachmentsView.addView(thumb);
|
|
|
|
|
|
|
|
|
|
DraftMediaAttachment draft=new DraftMediaAttachment();
|
|
|
|
|
draft.uri=uri;
|
|
|
|
|
draft.view=thumb;
|
|
|
|
|
draft.progressBar=thumb.findViewById(R.id.progress);
|
|
|
|
|
Button btn=thumb.findViewById(R.id.remove_btn);
|
|
|
|
|
btn.setTag(draft);
|
|
|
|
|
btn.setOnClickListener(this::onRemoveMediaAttachmentClick);
|
|
|
|
|
|
|
|
|
|
if(uploadingAttachment==null){
|
|
|
|
|
uploadMediaAttachment(draft);
|
|
|
|
|
}else{
|
|
|
|
|
queuedAttachments.add(draft);
|
|
|
|
|
}
|
|
|
|
|
updatePublishButtonState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void uploadMediaAttachment(DraftMediaAttachment attachment){
|
|
|
|
|
if(uploadingAttachment!=null)
|
|
|
|
|
throw new IllegalStateException("there is already an attachment being uploaded");
|
|
|
|
|
uploadingAttachment=attachment;
|
|
|
|
|
attachment.uploadRequest=(UploadAttachment) new UploadAttachment(attachment.uri)
|
|
|
|
|
.setProgressListener(new ProgressListener(){
|
|
|
|
|
@Override
|
|
|
|
|
public void onProgress(long transferred, long total){
|
|
|
|
|
int progress=Math.round(transferred/(float)total*attachment.progressBar.getMax());
|
|
|
|
|
if(Build.VERSION.SDK_INT>=24)
|
|
|
|
|
attachment.progressBar.setProgress(progress, true);
|
|
|
|
|
else
|
|
|
|
|
attachment.progressBar.setProgress(progress);
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.setCallback(new Callback<>(){
|
|
|
|
|
@Override
|
|
|
|
|
public void onSuccess(Attachment result){
|
|
|
|
|
attachment.serverAttachment=result;
|
|
|
|
|
attachment.uploadRequest=null;
|
|
|
|
|
uploadingAttachment=null;
|
|
|
|
|
attachments.add(attachment);
|
|
|
|
|
attachment.progressBar.setVisibility(View.GONE);
|
|
|
|
|
if(!queuedAttachments.isEmpty())
|
|
|
|
|
uploadMediaAttachment(queuedAttachments.remove(0));
|
|
|
|
|
updatePublishButtonState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onError(ErrorResponse error){
|
|
|
|
|
attachment.uploadRequest=null;
|
|
|
|
|
uploadingAttachment=null;
|
|
|
|
|
failedAttachments.add(attachment);
|
|
|
|
|
error.showToast(getActivity());
|
|
|
|
|
// TODO show the error state in the attachment view
|
|
|
|
|
|
|
|
|
|
if(!queuedAttachments.isEmpty())
|
|
|
|
|
uploadMediaAttachment(queuedAttachments.remove(0));
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.exec(accountID);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void onRemoveMediaAttachmentClick(View v){
|
|
|
|
|
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
|
|
|
|
|
if(att==uploadingAttachment){
|
|
|
|
|
att.uploadRequest.cancel();
|
|
|
|
|
if(!queuedAttachments.isEmpty())
|
|
|
|
|
uploadMediaAttachment(queuedAttachments.remove(0));
|
|
|
|
|
}else{
|
|
|
|
|
attachments.remove(att);
|
|
|
|
|
queuedAttachments.remove(att);
|
|
|
|
|
failedAttachments.remove(att);
|
|
|
|
|
}
|
|
|
|
|
attachmentsView.removeView(att.view);
|
|
|
|
|
updatePublishButtonState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static class DraftMediaAttachment{
|
|
|
|
|
public Attachment serverAttachment;
|
|
|
|
|
public Uri uri;
|
|
|
|
|
public UploadAttachment uploadRequest;
|
|
|
|
|
|
|
|
|
|
public View view;
|
|
|
|
|
public ProgressBar progressBar;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|