package app.fedilab.android.mastodon.activities; /* Copyright 2021 Thomas Schneider * * This file is a part of Fedilab * * 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. * * Fedilab 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 Fedilab; if not, * see . */ import static app.fedilab.android.BaseMainActivity.currentAccount; import static app.fedilab.android.BaseMainActivity.currentInstance; import static app.fedilab.android.BaseMainActivity.emojis; import android.Manifest; import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.Editable; import android.text.InputFilter; import android.text.TextWatcher; import android.util.TypedValue; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.widget.LinearLayout; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.lifecycle.ViewModelProvider; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.work.Data; import androidx.work.OneTimeWorkRequest; import androidx.work.OutOfQuotaPolicy; import androidx.work.WorkManager; import com.bumptech.glide.Glide; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.io.File; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.TimeUnit; import app.fedilab.android.BaseMainActivity; import app.fedilab.android.R; import app.fedilab.android.activities.MainActivity; import app.fedilab.android.databinding.ActivityPaginationBinding; import app.fedilab.android.databinding.PopupContactBinding; import app.fedilab.android.mastodon.client.entities.api.Account; import app.fedilab.android.mastodon.client.entities.api.Attachment; import app.fedilab.android.mastodon.client.entities.api.Context; import app.fedilab.android.mastodon.client.entities.api.EmojiInstance; import app.fedilab.android.mastodon.client.entities.api.Instance; import app.fedilab.android.mastodon.client.entities.api.Mention; import app.fedilab.android.mastodon.client.entities.api.ScheduledStatus; import app.fedilab.android.mastodon.client.entities.api.Status; import app.fedilab.android.mastodon.client.entities.app.BaseAccount; import app.fedilab.android.mastodon.client.entities.app.StatusDraft; import app.fedilab.android.mastodon.exception.DBException; import app.fedilab.android.mastodon.helper.DividerDecorationSimple; import app.fedilab.android.mastodon.helper.Helper; import app.fedilab.android.mastodon.helper.MastodonHelper; import app.fedilab.android.mastodon.helper.MediaHelper; import app.fedilab.android.mastodon.interfaces.OnDownloadInterface; import app.fedilab.android.mastodon.jobs.ComposeWorker; import app.fedilab.android.mastodon.jobs.ScheduleThreadWorker; import app.fedilab.android.mastodon.services.ThreadMessageService; import app.fedilab.android.mastodon.ui.drawer.AccountsReplyAdapter; import app.fedilab.android.mastodon.ui.drawer.ComposeAdapter; import app.fedilab.android.mastodon.viewmodel.mastodon.AccountsVM; import app.fedilab.android.mastodon.viewmodel.mastodon.StatusesVM; import es.dmoral.toasty.Toasty; public class ComposeActivity extends BaseActivity implements ComposeAdapter.ManageDrafts, AccountsReplyAdapter.ActionDone, ComposeAdapter.promptDraftListener, ComposeAdapter.MediaDescriptionCallBack { public static final int MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 754; public static final int REQUEST_AUDIO_PERMISSION_RESULT = 1653; public static final int PICK_MEDIA = 5700; public static final int TAKE_PHOTO = 5600; private final Timer timer = new Timer(); private List statusList; private Status statusReply, statusMention, statusQuoted; private StatusDraft statusDraft; private ActionBar actionBar; private ComposeAdapter composeAdapter; private final BroadcastReceiver imageReceiver = new BroadcastReceiver() { @Override public void onReceive(android.content.Context context, Intent intent) { String imgpath = intent.getStringExtra("imgpath"); float focusX = intent.getFloatExtra("focusX", -2); float focusY = intent.getFloatExtra("focusY", -2); if (imgpath != null) { int position = 0; for (Status status : statusList) { if (status.media_attachments != null && status.media_attachments.size() > 0) { for (Attachment attachment : status.media_attachments) { if (attachment.local_path != null && attachment.local_path.equalsIgnoreCase(imgpath)) { if (focusX != -2) { attachment.focus = focusX + "," + focusY; } composeAdapter.notifyItemChanged(position); break; } } } position++; } } } }; private boolean promptSaveDraft; private boolean restoredDraft; private List sharedAttachments; private ActivityPaginationBinding binding; private BaseAccount account; private String instance, token; private Uri photoFileUri; private ScheduledStatus scheduledStatus; private String visibility; private Account accountMention; private String statusReplyId; private Account mentionBooster; private String sharedSubject, sharedContent, sharedTitle, sharedDescription, shareURL, sharedUrlMedia; private String editMessageId; private static int visibilityToNumber(String visibility) { return switch (visibility) { case "unlisted" -> 2; case "private" -> 1; case "direct" -> 0; default -> 3; }; } private static String visibilityToString(int visibility) { return switch (visibility) { case 2 -> "unlisted"; case 1 -> "private"; case 0 -> "direct"; default -> "public"; }; } public static String getVisibility(BaseAccount account, String defaultVisibility) { int tootVisibility = visibilityToNumber(defaultVisibility); if (account != null && account.mastodon_account != null && account.mastodon_account.source != null) { int userVisibility = visibilityToNumber(account.mastodon_account.source.privacy); if (tootVisibility > userVisibility) { return visibilityToString(userVisibility); } else { return visibilityToString(tootVisibility); } } return defaultVisibility; } @Override protected void onDestroy() { super.onDestroy(); if (timer != null) { timer.cancel(); } unregisterReceiver(imageReceiver); } private void storeDraftWarning() { if (statusDraft == null) { statusDraft = ComposeAdapter.prepareDraft(statusList, composeAdapter, account.instance, account.user_id); } if (canBeSent(statusDraft) != 0) { if (promptSaveDraft) { AlertDialog.Builder alt_bld = new MaterialAlertDialogBuilder(ComposeActivity.this); alt_bld.setMessage(R.string.save_draft); alt_bld.setPositiveButton(R.string.save, (dialog, id) -> { dialog.dismiss(); storeDraft(false); finish(); }); alt_bld.setNegativeButton(R.string.no, (dialog, id) -> { try { new StatusDraft(ComposeActivity.this).removeDraft(statusDraft); } catch (DBException e) { e.printStackTrace(); } dialog.dismiss(); finish(); }); AlertDialog alert = alt_bld.create(); alert.show(); } else { if (!restoredDraft) { try { new StatusDraft(ComposeActivity.this).removeDraft(statusDraft); } catch (DBException e) { e.printStackTrace(); } } finish(); } } else { finish(); } } /** * Intialize the common view for the context * * @param context {@link Context} */ private void initializeContextView(final Context context) { if (context == null) { return; } //Build the array of statuses statusList.addAll(0, context.ancestors); composeAdapter.setStatusCount(context.ancestors.size() + 1); composeAdapter.notifyItemRangeInserted(0, context.ancestors.size()); composeAdapter.notifyItemRangeChanged(0, statusList.size()); if (binding.recyclerView.getItemDecorationCount() > 0) { for (int i = 0; i < binding.recyclerView.getItemDecorationCount(); i++) { binding.recyclerView.removeItemDecorationAt(i); } } binding.recyclerView.addItemDecoration(new DividerDecorationSimple(ComposeActivity.this, statusList)); binding.recyclerView.scrollToPosition(composeAdapter.getItemCount() - 1); } /** * Intialize the common view for the context * * @param context {@link Context} */ private void initializeContextRedraftView(final Context context, Status initialStatus) { if (context == null) { return; } //Build the array of statuses statusList.addAll(0, context.ancestors); statusList.add(initialStatus); statusList.add(statusDraft.statusDraftList.get(0)); composeAdapter = new ComposeAdapter(statusList, context.ancestors.size(), account, accountMention, visibility, editMessageId); composeAdapter.mediaDescriptionCallBack = this; composeAdapter.promptDraftListener = this; composeAdapter.manageDrafts = this; LinearLayoutManager mLayoutManager = new LinearLayoutManager(ComposeActivity.this); binding.recyclerView.setLayoutManager(mLayoutManager); binding.recyclerView.setAdapter(composeAdapter); composeAdapter.setStatusCount(context.ancestors.size() + 1); binding.recyclerView.addItemDecoration(new DividerDecorationSimple(ComposeActivity.this, statusList)); binding.recyclerView.scrollToPosition(composeAdapter.getItemCount() - 1); } @Override public boolean onCreateOptionsMenu(@NonNull Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_compose, menu); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { MenuItem micro = menu.findItem(R.id.action_microphone); if (micro != null) { micro.setVisible(false); } } return true; } @SuppressLint("ClickableViewAccessibility") @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { storeDraftWarning(); return true; } else if (item.getItemId() == R.id.action_photo_camera) { photoFileUri = MediaHelper.dispatchTakePictureIntent(ComposeActivity.this); } else if (item.getItemId() == R.id.action_contacts) { AlertDialog.Builder builderSingle = new MaterialAlertDialogBuilder(ComposeActivity.this); builderSingle.setTitle(getString(R.string.select_accounts)); PopupContactBinding popupContactBinding = PopupContactBinding.inflate(getLayoutInflater(), new LinearLayout(ComposeActivity.this), false); popupContactBinding.loader.setVisibility(View.VISIBLE); AccountsVM accountsVM = new ViewModelProvider(ComposeActivity.this).get(AccountsVM.class); accountsVM.searchAccounts(instance, token, "", 10, false, true) .observe(ComposeActivity.this, accounts -> onRetrieveContact(popupContactBinding, accounts)); popupContactBinding.searchAccount.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if (count > 0) { popupContactBinding.searchAccount.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_baseline_close_24, 0); } else { popupContactBinding.searchAccount.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_baseline_search_24, 0); } } @Override public void afterTextChanged(Editable s) { if (s != null && s.length() > 0) { accountsVM.searchAccounts(instance, token, s.toString().trim(), 10, false, true) .observe(ComposeActivity.this, accounts -> onRetrieveContact(popupContactBinding, accounts)); } } }); popupContactBinding.searchAccount.setOnTouchListener((v, event) -> { final int DRAWABLE_RIGHT = 2; if (event.getAction() == MotionEvent.ACTION_UP) { if (popupContactBinding.searchAccount.length() > 0 && event.getRawX() >= (popupContactBinding.searchAccount.getRight() - popupContactBinding.searchAccount.getCompoundDrawables()[DRAWABLE_RIGHT].getBounds().width())) { popupContactBinding.searchAccount.setText(""); accountsVM.searchAccounts(instance, token, "", 10, false, true) .observe(ComposeActivity.this, accounts -> onRetrieveContact(popupContactBinding, accounts)); } } return false; }); builderSingle.setView(popupContactBinding.getRoot()); builderSingle.setNegativeButton(R.string.validate, (dialog, which) -> { dialog.dismiss(); composeAdapter.putCursor(); }); builderSingle.show(); } else if (item.getItemId() == R.id.action_microphone) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { MediaHelper.recordAudio(ComposeActivity.this, file -> { List uris = new ArrayList<>(); uris.add(Uri.fromFile(new File(file))); composeAdapter.addAttachment(-1, uris); }); } else { if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) { Toast.makeText(this, getString(R.string.audio), Toast.LENGTH_SHORT).show(); } requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO }, REQUEST_AUDIO_PERMISSION_RESULT); } } else { MediaHelper.recordAudio(ComposeActivity.this, file -> { List uris = new ArrayList<>(); uris.add(Uri.fromFile(new File(file))); composeAdapter.addAttachment(-1, uris); }); } } else if (item.getItemId() == R.id.action_schedule) { if (statusDraft == null) { statusDraft = ComposeAdapter.prepareDraft(statusList, composeAdapter, account.instance, account.user_id); } if (canBeSent(statusDraft) == 1) { MediaHelper.scheduleMessage(ComposeActivity.this, date -> storeDraft(true, date)); } else if (canBeSent(statusDraft) == -1) { Toasty.warning(ComposeActivity.this, getString(R.string.toot_error_no_media_description), Toasty.LENGTH_SHORT).show(); } else if (canBeSent(statusDraft) == -2) { Toasty.warning(ComposeActivity.this, getString(R.string.toot_error_no_media_description), Toasty.LENGTH_SHORT).show(); MaterialAlertDialogBuilder materialAlertDialogBuilder = new MaterialAlertDialogBuilder(this); materialAlertDialogBuilder.setMessage(R.string.toot_error_no_media_description); materialAlertDialogBuilder.setPositiveButton(R.string.send_anyway, (dialog, id) -> { MediaHelper.scheduleMessage(ComposeActivity.this, date -> storeDraft(true, date)); dialog.dismiss(); }); materialAlertDialogBuilder.setNegativeButton(R.string.cancel, (dialog, id) -> { dialog.cancel(); }); AlertDialog alert = materialAlertDialogBuilder.create(); alert.show(); } else { Toasty.info(ComposeActivity.this, getString(R.string.toot_error_no_content), Toasty.LENGTH_SHORT).show(); } } else if (item.getItemId() == R.id.action_tags) { TagCacheActivity tagCacheActivity = new TagCacheActivity(); tagCacheActivity.show(getSupportFragmentManager(), null); } return true; } private void onRetrieveContact(PopupContactBinding popupContactBinding, List accounts) { popupContactBinding.loader.setVisibility(View.GONE); if (accounts == null) { accounts = new ArrayList<>(); } List checkedValues = new ArrayList<>(); List contacts = new ArrayList<>(accounts); for (Account account : contacts) { checkedValues.add(composeAdapter.getLastComposeContent().contains("@" + account.acct)); } AccountsReplyAdapter contactAdapter = new AccountsReplyAdapter(contacts, checkedValues); contactAdapter.actionDone = ComposeActivity.this; popupContactBinding.lvAccountsSearch.setAdapter(contactAdapter); popupContactBinding.lvAccountsSearch.setLayoutManager(new LinearLayoutManager(ComposeActivity.this)); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); List uris = new ArrayList<>(); if (requestCode >= PICK_MEDIA && resultCode == RESULT_OK) { ClipData clipData = data.getClipData(); int position = requestCode - PICK_MEDIA; if (clipData != null) { for (int i = 0; i < clipData.getItemCount(); i++) { ClipData.Item item = clipData.getItemAt(i); uris.add(item.getUri()); } } else { uris.add(data.getData()); } composeAdapter.addAttachment(position, uris); } else if (requestCode == TAKE_PHOTO && resultCode == RESULT_OK) { uris.add(photoFileUri); composeAdapter.addAttachment(-1, uris); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = ActivityPaginationBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); setSupportActionBar(binding.toolbar); promptSaveDraft = false; restoredDraft = false; actionBar = getSupportActionBar(); //Remove title if (actionBar != null) { actionBar.setDisplayShowTitleEnabled(false); } if (getSupportActionBar() != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true); } SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(this); float scale = sharedpreferences.getFloat(getString(R.string.SET_FONT_SCALE), 1.1f); binding.title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18 * 1.1f / scale); statusList = new ArrayList<>(); Bundle b = getIntent().getExtras(); if (b != null) { statusReply = (Status) b.getSerializable(Helper.ARG_STATUS_REPLY); statusQuoted = (Status) b.getSerializable(Helper.ARG_QUOTED_MESSAGE); statusDraft = (StatusDraft) b.getSerializable(Helper.ARG_STATUS_DRAFT); scheduledStatus = (ScheduledStatus) b.getSerializable(Helper.ARG_STATUS_SCHEDULED); statusReplyId = b.getString(Helper.ARG_STATUS_REPLY_ID); statusMention = (Status) b.getSerializable(Helper.ARG_STATUS_MENTION); account = (BaseAccount) b.getSerializable(Helper.ARG_ACCOUNT); if (account == null) { account = currentAccount; } editMessageId = b.getString(Helper.ARG_EDIT_STATUS_ID, null); instance = b.getString(Helper.ARG_INSTANCE, null); token = b.getString(Helper.ARG_TOKEN, null); visibility = b.getString(Helper.ARG_VISIBILITY, null); if (visibility == null && statusReply != null) { visibility = getVisibility(account, statusReply.visibility); } else if (visibility == null && currentAccount != null && currentAccount.mastodon_account != null && currentAccount.mastodon_account.source != null) { visibility = currentAccount.mastodon_account.source.privacy; } mentionBooster = (Account) b.getSerializable(Helper.ARG_MENTION_BOOSTER); accountMention = (Account) b.getSerializable(Helper.ARG_ACCOUNT_MENTION); //Shared elements sharedAttachments = (ArrayList) b.getSerializable(Helper.ARG_MEDIA_ATTACHMENTS); sharedUrlMedia = b.getString(Helper.ARG_SHARE_URL_MEDIA); sharedSubject = b.getString(Helper.ARG_SHARE_SUBJECT, null); sharedContent = b.getString(Helper.ARG_SHARE_CONTENT, null); sharedTitle = b.getString(Helper.ARG_SHARE_TITLE, null); sharedDescription = b.getString(Helper.ARG_SHARE_DESCRIPTION, null); shareURL = b.getString(Helper.ARG_SHARE_URL, null); } if (sharedContent != null && shareURL != null && sharedContent.compareTo(shareURL) == 0) { sharedContent = ""; } if (sharedTitle != null && sharedSubject != null && sharedSubject.length() > sharedTitle.length()) { sharedTitle = sharedSubject; } //Edit a scheduled status from server if (scheduledStatus != null) { statusDraft = new StatusDraft(); List statuses = new ArrayList<>(); Status status = new Status(); status.id = Helper.generateIdString(); status.text = scheduledStatus.params.text; status.in_reply_to_id = scheduledStatus.params.in_reply_to_id; status.poll = scheduledStatus.params.poll; if (scheduledStatus.params.media_ids != null && scheduledStatus.params.media_ids.size() > 0) { status.media_attachments = new ArrayList<>(); new Thread(() -> { StatusesVM statusesVM = new ViewModelProvider(ComposeActivity.this).get(StatusesVM.class); for (String attachmentId : scheduledStatus.params.media_ids) { statusesVM.getAttachment(instance, token, attachmentId) .observe(ComposeActivity.this, attachment -> status.media_attachments.add(attachment)); } }).start(); } status.sensitive = scheduledStatus.params.sensitive; status.spoiler_text = scheduledStatus.params.spoiler_text; status.visibility = scheduledStatus.params.visibility; statusDraft.statusDraftList = statuses; } if (account == null) { account = currentAccount; } if (account == null) { Toasty.error(ComposeActivity.this, getString(R.string.toast_error), Toasty.LENGTH_SHORT).show(); finish(); return; } if (instance == null) { instance = account.instance; } if (token == null) { token = account.token; } if (emojis == null || !emojis.containsKey(instance)) { new Thread(() -> { try { emojis.put(instance, new EmojiInstance(ComposeActivity.this).getEmojiList(instance)); } catch (DBException e) { e.printStackTrace(); } }).start(); } if (MainActivity.instanceInfo == null) { String instanceInfo = sharedpreferences.getString(getString(R.string.INSTANCE_INFO) + instance, null); if (instanceInfo != null) { MainActivity.instanceInfo = Instance.restore(instanceInfo); } } StatusesVM statusesVM = new ViewModelProvider(ComposeActivity.this).get(StatusesVM.class); //Empty compose List statusDraftList = new ArrayList<>(); Status status = new Status(); status.id = Helper.generateIdString(); if (statusQuoted != null) { status.quote_id = statusQuoted.id; } statusDraftList.add(status); if (statusReplyId != null && statusDraft != null) {//Delete and redraft statusesVM.getStatus(currentInstance, BaseMainActivity.currentToken, statusReplyId) .observe(ComposeActivity.this, status1 -> { if (status1 != null) { statusesVM.getContext(currentInstance, BaseMainActivity.currentToken, statusReplyId) .observe(ComposeActivity.this, statusContext -> { if (statusContext != null) { initializeContextRedraftView(statusContext, status1); } else { Helper.sendToastMessage(getApplication(), Helper.RECEIVE_TOAST_TYPE_ERROR, getString(R.string.toast_error)); } }); } else { Helper.sendToastMessage(getApplication(), Helper.RECEIVE_TOAST_TYPE_ERROR, getString(R.string.toast_error)); } }); } else if (statusDraft != null) {//Restore a draft with all messages restoredDraft = true; if (statusDraft.statusReplyList != null) { statusList.addAll(statusDraft.statusReplyList); binding.recyclerView.addItemDecoration(new DividerDecorationSimple(ComposeActivity.this, statusList)); } int statusCount = statusList.size(); statusList.addAll(statusDraft.statusDraftList); composeAdapter = new ComposeAdapter(statusList, statusCount, account, accountMention, visibility, editMessageId); composeAdapter.mediaDescriptionCallBack = this; composeAdapter.manageDrafts = this; composeAdapter.promptDraftListener = this; LinearLayoutManager mLayoutManager = new LinearLayoutManager(ComposeActivity.this); binding.recyclerView.setLayoutManager(mLayoutManager); binding.recyclerView.setAdapter(composeAdapter); binding.recyclerView.scrollToPosition(composeAdapter.getItemCount() - 1); } else if (statusReply != null) { statusList.add(statusReply); int statusCount = statusList.size(); statusDraftList.get(0).in_reply_to_id = statusReply.id; //We change order for mentions //At first place the account that has been mentioned if it's not our statusDraftList.get(0).mentions = new ArrayList<>(); if (statusReply.account.acct != null && account.mastodon_account != null && !statusReply.account.acct.equalsIgnoreCase(account.mastodon_account.acct)) { Mention mention = new Mention(); mention.acct = "@" + statusReply.account.acct; mention.url = statusReply.account.url; mention.username = statusReply.account.username; statusDraftList.get(0).mentions.add(mention); } //There are other mentions to if (statusReply.mentions != null && statusReply.mentions.size() > 0) { for (Mention mentionTmp : statusReply.mentions) { if (statusReply.account.acct != null && !mentionTmp.acct.equalsIgnoreCase(statusReply.account.acct) && account.mastodon_account != null && !mentionTmp.acct.equalsIgnoreCase(account.mastodon_account.acct)) { statusDraftList.get(0).mentions.add(mentionTmp); } } } if (mentionBooster != null) { Mention mention = new Mention(); mention.acct = mentionBooster.acct; mention.url = mentionBooster.url; mention.username = mentionBooster.username; boolean present = false; for (Mention mentionTmp : statusDraftList.get(0).mentions) { if (mentionTmp.acct.equalsIgnoreCase(mentionBooster.acct)) { present = true; break; } } if (!present) { statusDraftList.get(0).mentions.add(mention); } } if (statusReply.spoiler_text != null) { statusDraftList.get(0).spoiler_text = statusReply.spoiler_text; if (statusReply.spoiler_text.trim().length() > 0) { statusDraftList.get(0).spoilerChecked = true; } } if (statusReply.language != null && !statusReply.language.isEmpty()) { Set storedLanguages = sharedpreferences.getStringSet(getString(R.string.SET_SELECTED_LANGUAGE), null); if (storedLanguages == null || storedLanguages.size() == 0) { statusDraftList.get(0).language = statusReply.language; } else { if (storedLanguages.contains(statusReply.language)) { statusDraftList.get(0).language = statusReply.language; } else { String currentCode = sharedpreferences.getString(getString(R.string.SET_COMPOSE_LANGUAGE) + account.user_id + account.instance, Locale.getDefault().getLanguage()); if (currentCode.isEmpty()) { currentCode = "EN"; } statusDraftList.get(0).language = currentCode; } } } //StatusDraftList at this point should only have one element statusList.addAll(statusDraftList); composeAdapter = new ComposeAdapter(statusList, statusCount, account, accountMention, visibility, editMessageId); composeAdapter.mediaDescriptionCallBack = this; composeAdapter.manageDrafts = this; composeAdapter.promptDraftListener = this; LinearLayoutManager mLayoutManager = new LinearLayoutManager(ComposeActivity.this); binding.recyclerView.setLayoutManager(mLayoutManager); binding.recyclerView.setAdapter(composeAdapter); statusesVM.getContext(currentInstance, BaseMainActivity.currentToken, statusReply.id) .observe(ComposeActivity.this, this::initializeContextView); } else if (statusQuoted != null) { statusList.add(statusQuoted); int statusCount = statusList.size(); statusDraftList.get(0).quote_id = statusQuoted.id; //StatusDraftList at this point should only have one element statusList.addAll(statusDraftList); composeAdapter = new ComposeAdapter(statusList, statusCount, account, accountMention, visibility, editMessageId); composeAdapter.mediaDescriptionCallBack = this; composeAdapter.manageDrafts = this; composeAdapter.promptDraftListener = this; LinearLayoutManager mLayoutManager = new LinearLayoutManager(ComposeActivity.this); binding.recyclerView.setLayoutManager(mLayoutManager); binding.recyclerView.setAdapter(composeAdapter); } else { //Compose without replying statusList.addAll(statusDraftList); composeAdapter = new ComposeAdapter(statusList, 0, account, accountMention, visibility, editMessageId); composeAdapter.mediaDescriptionCallBack = this; composeAdapter.manageDrafts = this; composeAdapter.promptDraftListener = this; LinearLayoutManager mLayoutManager = new LinearLayoutManager(ComposeActivity.this); binding.recyclerView.setLayoutManager(mLayoutManager); binding.recyclerView.setAdapter(composeAdapter); if (statusMention != null) { composeAdapter.loadMentions(statusMention); } } MastodonHelper.loadPPMastodon(binding.profilePicture, account.mastodon_account); ContextCompat.registerReceiver(ComposeActivity.this, imageReceiver, new IntentFilter(Helper.INTENT_SEND_MODIFIED_IMAGE), ContextCompat.RECEIVER_NOT_EXPORTED); if (timer != null) { timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { if (promptSaveDraft) { storeDraft(false); } } }, 0, 10000); } if (sharedAttachments != null && sharedAttachments.size() > 0) { for (Attachment attachment : sharedAttachments) { composeAdapter.addAttachment(-1, attachment); } } /*else if (sharedUri != null && !sharedUri.toString().startsWith("http")) { List uris = new ArrayList<>(); uris.add(sharedUri); Helper.createAttachmentFromUri(ComposeActivity.this, uris, attachments -> { for(Attachment attachment: attachments) { composeAdapter.addAttachment(-1, attachment); } }); } */ else if (shareURL != null) { Helper.download(ComposeActivity.this, sharedUrlMedia, new OnDownloadInterface() { @Override public void onDownloaded(String saveFilePath, String downloadUrl, Error error) { composeAdapter.addSharing(shareURL, sharedTitle, sharedDescription, sharedSubject, sharedContent, saveFilePath); } @Override public void onUpdateProgress(int progress) { } }); } else { if (composeAdapter != null) { composeAdapter.addSharing(null, null, sharedDescription, null, sharedContent, null); } } getOnBackPressedDispatcher().addCallback(new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { if (binding.recyclerView.getVisibility() == View.VISIBLE) { storeDraftWarning(); } } }); } @Override public void onItemDraftAdded(int position, String initialContent) { Status status = new Status(); status.id = Helper.generateIdString(); status.mentions = statusList.get(position - 1).mentions; status.visibility = statusList.get(position - 1).visibility; if(initialContent != null) { status.text = initialContent; } final SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(ComposeActivity.this); boolean unlistedReplies = sharedpreferences.getBoolean(getString(R.string.SET_UNLISTED_REPLIES), true); if (status.visibility.equalsIgnoreCase("public") && unlistedReplies) { status.visibility = "unlisted"; } status.spoiler_text = statusList.get(position - 1).spoiler_text; status.sensitive = statusList.get(position - 1).sensitive; status.spoilerChecked = statusList.get(position - 1).spoilerChecked; statusList.add(status); composeAdapter.notifyItemInserted(position); composeAdapter.notifyItemRangeChanged(0, statusList.size()); binding.recyclerView.smoothScrollToPosition(statusList.size()); } @Override public void onItemDraftDeleted(Status status, int position) { statusList.remove(status); composeAdapter.notifyItemRemoved(position); } @Override public void onSubmit(StatusDraft draft) { //Store in drafts if (statusDraft == null) { statusDraft = draft; } else { statusDraft.statusDraftList = draft.statusDraftList; } storeDraft(true); } private void storeDraft(boolean sendMessage) { storeDraft(sendMessage, null); } private void storeDraft(boolean sendMessage, String scheduledDate) { new Thread(() -> { //Collect all statusCompose List statusDrafts = new ArrayList<>(); List statusReplies = new ArrayList<>(); for (Status status : statusList) { if (status.id == null || status.id.startsWith("@fedilab_compose_")) { statusDrafts.add(status); } else { statusReplies.add(status); } } if (statusDraft == null) { statusDraft = new StatusDraft(ComposeActivity.this); } else { //Draft previously and date is changed if (statusDraft.scheduled_at != null && scheduledDate != null && statusDraft.workerUuid != null) { WorkManager.getInstance(ComposeActivity.this).cancelWorkById(statusDraft.workerUuid); } } if (statusReplies.size() > 0) { statusDraft.statusReplyList = new ArrayList<>(); statusDraft.statusReplyList.addAll(statusReplies); } if (statusDrafts.size() > 0) { statusDraft.statusDraftList = new ArrayList<>(); statusDraft.statusDraftList.addAll(statusDrafts); } if (statusDraft.instance == null) { statusDraft.instance = account.instance; } if (statusDraft.user_id == null) { statusDraft.user_id = account.user_id; } if (canBeSent(statusDraft) != 1 && sendMessage) { Handler mainHandler = new Handler(Looper.getMainLooper()); Runnable myRunnable = () -> { if (canBeSent(statusDraft) == -1) { Toasty.warning(ComposeActivity.this, getString(R.string.toot_error_no_media_description), Toasty.LENGTH_SHORT).show(); } else if (canBeSent(statusDraft) == -2) { MaterialAlertDialogBuilder materialAlertDialogBuilder = new MaterialAlertDialogBuilder(this); materialAlertDialogBuilder.setMessage(R.string.toot_error_no_media_description); materialAlertDialogBuilder.setPositiveButton(R.string.send_anyway, (dialog, id) -> { sendMessage(true, scheduledDate); dialog.dismiss(); }); materialAlertDialogBuilder.setNegativeButton(R.string.add_description, (dialog, id) -> { composeAdapter.openMissingDescription(); dialog.cancel(); }); AlertDialog alert = materialAlertDialogBuilder.create(); alert.show(); } else { Toasty.info(ComposeActivity.this, getString(R.string.toot_error_no_content), Toasty.LENGTH_SHORT).show(); } if (statusDrafts.size() > 0) { statusDrafts.get(statusDrafts.size() - 1).submitted = false; composeAdapter.notifyItemChanged(statusList.size() - 1); } }; mainHandler.post(myRunnable); return; } sendMessage(sendMessage, scheduledDate); }).start(); } private void sendMessage(boolean sendMessage, String scheduledDate) { if (statusDraft.id > 0) { try { new StatusDraft(ComposeActivity.this).updateStatusDraft(statusDraft); } catch (DBException e) { e.printStackTrace(); } } else { try { statusDraft.id = new StatusDraft(ComposeActivity.this).insertStatusDraft(statusDraft); } catch (DBException e) { e.printStackTrace(); } } //Only one single message scheduled if (sendMessage && scheduledDate != null && statusDraft.statusDraftList.size() > 1) { //Schedule a thread SimpleDateFormat sdf = new SimpleDateFormat(Helper.SCHEDULE_DATE_FORMAT, Locale.getDefault()); Date date; try { date = sdf.parse(scheduledDate); long delayToPass = 0; if (date != null) { delayToPass = (date.getTime() - new Date().getTime()); } Data inputData = new Data.Builder() .putString(Helper.ARG_INSTANCE, currentInstance) .putString(Helper.ARG_TOKEN, BaseMainActivity.currentToken) .putString(Helper.ARG_USER_ID, BaseMainActivity.currentUserID) .putLong(Helper.ARG_STATUS_DRAFT_ID, statusDraft.id) .build(); OneTimeWorkRequest oneTimeWorkRequest = new OneTimeWorkRequest.Builder(ScheduleThreadWorker.class) .setInputData(inputData) .addTag(Helper.WORKER_SCHEDULED_STATUSES) .setInitialDelay(delayToPass, TimeUnit.MILLISECONDS) .build(); WorkManager.getInstance(ComposeActivity.this).enqueue(oneTimeWorkRequest); statusDraft.workerUuid = oneTimeWorkRequest.getId(); statusDraft.scheduled_at = date; try { new StatusDraft(ComposeActivity.this).updateStatusDraft(statusDraft); } catch (DBException e) { e.printStackTrace(); } Handler mainHandler = new Handler(Looper.getMainLooper()); Runnable myRunnable = () -> { Toasty.info(ComposeActivity.this, getString(R.string.toot_scheduled), Toasty.LENGTH_LONG).show(); finish(); }; mainHandler.post(myRunnable); } catch (ParseException e) { e.printStackTrace(); } } else if (sendMessage) { int mediaCount = 0; for (Status status : statusDraft.statusDraftList) { mediaCount += status.media_attachments != null ? status.media_attachments.size() : 0; } if (mediaCount > 0) { Data inputData = new Data.Builder() .putString(Helper.ARG_STATUS_DRAFT_ID, String.valueOf(statusDraft.id)) .putString(Helper.ARG_INSTANCE, instance) .putString(Helper.ARG_TOKEN, token) .putString(Helper.ARG_EDIT_STATUS_ID, editMessageId) .putString(Helper.ARG_USER_ID, account.user_id) .putString(Helper.ARG_SCHEDULED_DATE, scheduledDate).build(); OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(ComposeWorker.class) .setInputData(inputData) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .build(); WorkManager.getInstance(ComposeActivity.this).enqueue(request); } else { new ThreadMessageService(ComposeActivity.this, instance, account.user_id, token, statusDraft, scheduledDate, editMessageId); } finish(); } } private int canBeSent(StatusDraft statusDraft) { if (statusDraft == null) { return 0; } SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(this); boolean checkAlt = sharedpreferences.getBoolean(getString(R.string.SET_MANDATORY_ALT_TEXT), true); boolean warnOnly = sharedpreferences.getBoolean(getString(R.string.SET_MANDATORY_ALT_TEXT_WARN), true); if (checkAlt) { for (Status status : statusDraft.statusDraftList) { if (status != null && status.media_attachments != null && status.media_attachments.size() > 0) { for (Attachment attachment : status.media_attachments) { if (attachment.description == null || attachment.description.trim().isEmpty()) { return warnOnly ? -2 : -1; } } } } } List statuses = statusDraft.statusDraftList; if (statuses == null || statuses.size() == 0) { return 0; } Status statusCheck = statuses.get(0); if (statusCheck == null) { return 0; } return (statusCheck.text != null && statusCheck.text.trim().length() != 0) || (statusCheck.media_attachments != null && statusCheck.media_attachments.size() != 0) || statusCheck.poll != null || (statusCheck.spoiler_text != null && statusCheck.spoiler_text.trim().length() != 0) ? 1 : 0; } @Override public void onContactClick(boolean isChecked, String acct) { composeAdapter.updateContent(isChecked, acct); } @Override public void promptDraft() { promptSaveDraft = true; } @Override public void click(ComposeAdapter.ComposeViewHolder holder, Attachment attachment, int messagePosition, int mediaPosition) { binding.description.setVisibility(View.VISIBLE); actionBar.hide(); binding.recyclerView.setVisibility(View.GONE); binding.mediaDescription.setText(""); String attachmentPath = attachment.local_path != null && !attachment.local_path.trim().isEmpty() ? attachment.local_path : attachment.preview_url; Glide.with(binding.mediaPreview.getContext()) .load(attachmentPath) .into(binding.mediaPreview); if (attachment.description != null) { binding.mediaDescription.setText(attachment.description); binding.mediaDescription.setSelection(Objects.requireNonNull(binding.mediaDescription.getText()).length()); } binding.mediaDescription.setFilters(new InputFilter[]{new InputFilter.LengthFilter(1500)}); binding.mediaDescription.requestFocus(); Objects.requireNonNull(getWindow()).setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); binding.mediaDescription.requestFocus(); binding.mediaSave.setOnClickListener(v -> { binding.description.setVisibility(View.GONE); actionBar.show(); binding.recyclerView.setVisibility(View.VISIBLE); composeAdapter.openDescriptionActivity(true, Objects.requireNonNull(binding.mediaDescription.getText()).toString().trim(), holder, attachment, messagePosition, mediaPosition); }); binding.mediaCancel.setOnClickListener(v -> { binding.description.setVisibility(View.GONE); actionBar.show(); binding.recyclerView.setVisibility(View.VISIBLE); composeAdapter.openDescriptionActivity(false, Objects.requireNonNull(binding.mediaDescription.getText()).toString().trim(), holder, attachment, messagePosition, mediaPosition); }); } public enum mediaType { PHOTO, VIDEO, AUDIO, ALL } }