package app.fedilab.android.ui.drawer; /* 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.instanceInfo; import static app.fedilab.android.activities.ComposeActivity.MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE; import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.text.Editable; import android.text.Html; import android.text.InputFilter; import android.text.SpannableString; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.GridView; import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.AppCompatEditText; import androidx.appcompat.widget.LinearLayoutCompat; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.core.widget.ImageViewCompat; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelStoreOwner; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; import java.io.File; import java.text.Normalizer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import app.fedilab.android.BaseMainActivity; import app.fedilab.android.R; import app.fedilab.android.activities.ComposeActivity; import app.fedilab.android.client.entities.Account; import app.fedilab.android.client.entities.StatusDraft; import app.fedilab.android.client.mastodon.entities.Attachment; import app.fedilab.android.client.mastodon.entities.Emoji; import app.fedilab.android.client.mastodon.entities.EmojiInstance; import app.fedilab.android.client.mastodon.entities.Mention; import app.fedilab.android.client.mastodon.entities.Poll; import app.fedilab.android.client.mastodon.entities.Status; import app.fedilab.android.client.mastodon.entities.Tag; import app.fedilab.android.databinding.ComposeAttachmentItemBinding; import app.fedilab.android.databinding.ComposePollBinding; import app.fedilab.android.databinding.ComposePollItemBinding; import app.fedilab.android.databinding.DrawerStatusComposeBinding; import app.fedilab.android.databinding.DrawerStatusSimpleBinding; import app.fedilab.android.databinding.PopupMediaDescriptionBinding; import app.fedilab.android.exception.DBException; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; import app.fedilab.android.viewmodel.mastodon.AccountsVM; import app.fedilab.android.viewmodel.mastodon.SearchVM; import es.dmoral.toasty.Toasty; public class ComposeAdapter extends RecyclerView.Adapter { private static final int searchDeep = 15; private static final int TYPE_COMPOSE = 1; public static boolean autocomplete = false; public static String[] ALPHA = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "!", ",", "?", ".", "'"}; public static String[] MORSE = {".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..", "--", "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-", "-.--", "--..", ".----", "..---", "...--", "....-", ".....", "-....", "--...", "---..", "----.", "-----", "-.-.--", "--..--", "..--..", ".-.-.-", ".----.",}; private final List statusList; private final int TYPE_NORMAL = 0; private final Account account; public ManageDrafts manageDrafts; List emojis; private int statusCount; private Context context; private AlertDialog alertDialogEmoji; private final String visibility; private final app.fedilab.android.client.mastodon.entities.Account mentionedAccount; public ComposeAdapter(List statusList, int statusCount, Account account, app.fedilab.android.client.mastodon.entities.Account mentionedAccount, String visibility) { this.statusList = statusList; this.statusCount = statusCount; this.account = account; this.mentionedAccount = mentionedAccount; this.visibility = visibility; } private static void updateCharacterCount(ComposeViewHolder composeViewHolder) { int charCount = MastodonHelper.countLength(composeViewHolder); composeViewHolder.binding.characterCount.setText(String.valueOf(charCount)); composeViewHolder.binding.characterProgress.setProgress(charCount); } public static StatusDraft prepareDraft(List statusList, ComposeAdapter composeAdapter, String instance, String user_id) { //Collect all statusCompose List statusDrafts = new ArrayList<>(); List statusReplies = new ArrayList<>(); int i = 0; for (Status status : statusList) { //Statuses must be sent if (composeAdapter.getItemViewType(i) == TYPE_COMPOSE) { statusDrafts.add(status); } else { statusReplies.add(status); } i++; } StatusDraft statusDraftDB = new StatusDraft(); statusDraftDB.statusReplyList = statusReplies; statusDraftDB.statusDraftList = statusDrafts; statusDraftDB.instance = instance; statusDraftDB.user_id = user_id; return statusDraftDB; } //Create text when mentioning a toot public void loadMentions(Status status) { //Get the first draft statusList.get(statusCount).text = String.format("\n\nvia @%s\n\n%s\n\n", status.account.acct, status.url); notifyItemChanged(statusCount); } /** * Manage mentions displayed when replying to a message * * @param context Context * @param statusDraft {@link Status} - Status that user is replying * @param holder {@link ComposeViewHolder} - current compose viewHolder */ private void manageMentions(Context context, Status statusDraft, ComposeViewHolder holder) { if (statusDraft.mentions != null && (statusDraft.text == null || statusDraft.text.length() == 0)) { //Retrieves mentioned accounts + OP and adds them at the beginin of the toot final SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); Mention inReplyToUser = null; for (Mention mention : statusDraft.mentions) { //Mentioned account has a null id if (mention.id == null) { inReplyToUser = mention; break; } } if (statusDraft.text == null) { statusDraft.text = ""; } //Put other accounts mentioned at the bottom boolean capitalize = sharedpreferences.getBoolean(context.getString(R.string.SET_CAPITALIZE), true); if (inReplyToUser != null) { if (capitalize) { statusDraft.text = inReplyToUser.acct + "\n"; } else { statusDraft.text = inReplyToUser.acct + " "; } } holder.binding.content.setText(statusDraft.text); statusDraft.cursorPosition = statusDraft.text.length(); if (statusDraft.mentions.size() > 1) { statusDraft.text += "\n"; for (Mention mention : statusDraft.mentions) { if (mention.id != null && mention.acct != null && !mention.id.equals(BaseMainActivity.currentUserID)) { String tootTemp = String.format("@%s ", mention.acct); statusDraft.text = String.format("%s ", (statusDraft.text + tootTemp.trim())); } } } holder.binding.content.setText(statusDraft.text); updateCharacterCount(holder); holder.binding.content.requestFocus(); holder.binding.content.post(() -> { holder.binding.content.setSelection(statusDraft.cursorPosition); //Put cursor at the end buttonVisibility(holder); }); } else if (mentionedAccount != null) { final SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); boolean capitalize = sharedpreferences.getBoolean(context.getString(R.string.SET_CAPITALIZE), true); if (capitalize) { statusDraft.text = mentionedAccount.acct + "\n"; } else { statusDraft.text = mentionedAccount.acct + " "; } holder.binding.content.setText(statusDraft.text); updateCharacterCount(holder); holder.binding.content.requestFocus(); holder.binding.content.post(() -> { buttonVisibility(holder); holder.binding.content.setSelection(statusDraft.text.length()); //Put cursor at the end }); } else { holder.binding.content.requestFocus(); } } public void setStatusCount(int count) { statusCount = count; } public int getCount() { return (statusList.size()); } public Status getItem(int position) { return statusList.get(position); } @Override public int getItemViewType(int position) { return position >= statusCount ? TYPE_COMPOSE : TYPE_NORMAL; } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { context = parent.getContext(); if (viewType == TYPE_NORMAL) { DrawerStatusSimpleBinding itemBinding = DrawerStatusSimpleBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); return new StatusSimpleViewHolder(itemBinding); } else { DrawerStatusComposeBinding itemBinding = DrawerStatusComposeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); return new ComposeViewHolder(itemBinding); } } private void pickupMedia(ComposeActivity.mediaType type, int position) { if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE); return; } Intent intent; intent = new Intent(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); String[] mimetypes = new String[0]; if (type == ComposeActivity.mediaType.PHOTO) { if (instanceInfo.getMimeTypeImage().size() > 0) { mimetypes = instanceInfo.getMimeTypeImage().toArray(new String[0]); } else { mimetypes = new String[]{"image/*"}; } } else if (type == ComposeActivity.mediaType.VIDEO) { if (instanceInfo.getMimeTypeVideo().size() > 0) { mimetypes = instanceInfo.getMimeTypeVideo().toArray(new String[0]); } else { mimetypes = new String[]{"video/*"}; } } else if (type == ComposeActivity.mediaType.AUDIO) { if (instanceInfo.getMimeTypeAudio().size() > 0) { mimetypes = instanceInfo.getMimeTypeAudio().toArray(new String[0]); } else { mimetypes = new String[]{"audio/mpeg", "audio/opus", "audio/flac", "audio/wav", "audio/ogg"}; } } else if (type == ComposeActivity.mediaType.ALL) { if (instanceInfo.getMimeTypeOther().size() > 0) { mimetypes = instanceInfo.getMimeTypeOther().toArray(new String[0]); } else { mimetypes = new String[]{"*/*"}; } } intent.putExtra(Intent.EXTRA_MIME_TYPES, mimetypes); ((Activity) context).startActivityForResult(intent, (ComposeActivity.PICK_MEDIA + position)); } /** * Manage the visibility of the button (+/-) for adding a message to the composed thread * * @param holder - ComposeViewHolder */ private void buttonVisibility(ComposeViewHolder holder) { //First message - Needs at least one char to display the + button if (holder.getLayoutPosition() == statusCount && canBeRemoved(statusList.get(holder.getLayoutPosition()))) { holder.binding.addRemoveStatus.setVisibility(View.GONE); return; } //Manage last compose drawer button visibility if (holder.getLayoutPosition() == (getItemCount() - 1)) { if (statusList.size() > statusCount + 1) { if (canBeRemoved(statusList.get(statusList.size() - 1))) { holder.binding.addRemoveStatus.setImageResource(R.drawable.ic_compose_thread_remove_status); holder.binding.addRemoveStatus.setContentDescription(context.getString(R.string.remove_status)); holder.binding.addRemoveStatus.setOnClickListener(v -> { manageDrafts.onItemDraftDeleted(statusList.get(holder.getLayoutPosition()), holder.getLayoutPosition()); notifyItemChanged((getItemCount() - 1)); }); } else { holder.binding.addRemoveStatus.setImageResource(R.drawable.ic_compose_thread_add_status); holder.binding.addRemoveStatus.setContentDescription(context.getString(R.string.add_status)); holder.binding.addRemoveStatus.setOnClickListener(v -> { manageDrafts.onItemDraftAdded(holder.getLayoutPosition()); buttonVisibility(holder); }); } } else { holder.binding.addRemoveStatus.setImageResource(R.drawable.ic_compose_thread_add_status); holder.binding.addRemoveStatus.setContentDescription(context.getString(R.string.add_status)); holder.binding.addRemoveStatus.setOnClickListener(v -> { manageDrafts.onItemDraftAdded(holder.getLayoutPosition()); buttonVisibility(holder); }); } holder.binding.addRemoveStatus.setVisibility(View.VISIBLE); holder.binding.buttonPost.setVisibility(View.VISIBLE); } else { holder.binding.addRemoveStatus.setVisibility(View.GONE); holder.binding.buttonPost.setVisibility(View.GONE); } } /** * Check content of the draft to set if it can be removed (empty poll / media / text / spoiler) * * @param draft - Status * @return boolean */ private boolean canBeRemoved(Status draft) { return draft.poll == null && (draft.media_attachments == null || draft.media_attachments.size() == 0) && (draft.text == null || draft.text.trim().length() == 0) && (draft.spoiler_text == null || draft.spoiler_text.trim().length() == 0); } /** * Add an attachment from ComposeActivity * * @param position int - position of the drawer that added a media * @param uris List - uris of the media */ public void addAttachment(int position, List uris) { if (position == -1) { position = statusList.size() - 1; } if (statusList.get(position).media_attachments == null) { statusList.get(position).media_attachments = new ArrayList<>(); } int finalPosition = position; Helper.createAttachmentFromUri(context, uris, attachment -> { statusList.get(finalPosition).media_attachments.add(attachment); notifyItemChanged(finalPosition); }); } //<------ Manage contact from compose activity //It only targets last message in a thread //Return content of last compose message public String getLastComposeContent() { return statusList.get(statusList.size() - 1).text != null ? statusList.get(statusList.size() - 1).text : ""; } //Used to write contact when composing public void updateContent(boolean checked, String acct) { if (checked) { if (!statusList.get(statusList.size() - 1).text.contains(acct)) statusList.get(statusList.size() - 1).text = String.format("%s %s", acct, statusList.get(statusList.size() - 1).text); } else { statusList.get(statusList.size() - 1).text = statusList.get(statusList.size() - 1).text.replaceAll("\\s*" + acct, ""); } notifyItemChanged(statusList.size() - 1); } //------- end contact -----> //Put cursor to the end after changing contacts public void putCursor() { statusList.get(statusList.size() - 1).setCursorToEnd = true; notifyItemChanged(statusList.size() - 1); } private void displayAttachments(ComposeViewHolder holder, int position, int scrollToMediaPosition) { if (statusList.size() > position && statusList.get(position).media_attachments != null) { holder.binding.attachmentsList.removeAllViews(); List attachmentList = statusList.get(position).media_attachments; if (attachmentList != null && attachmentList.size() > 0) { holder.binding.sensitiveMedia.setVisibility(View.VISIBLE); holder.binding.sensitiveMedia.setChecked(BaseMainActivity.accountWeakReference.get().mastodon_account.source.sensitive); statusList.get(position).sensitive = BaseMainActivity.accountWeakReference.get().mastodon_account.source.sensitive; holder.binding.sensitiveMedia.setOnCheckedChangeListener((buttonView, isChecked) -> statusList.get(position).sensitive = isChecked); int mediaPosition = 0; for (Attachment attachment : attachmentList) { ComposeAttachmentItemBinding composeAttachmentItemBinding = ComposeAttachmentItemBinding.inflate(LayoutInflater.from(context), holder.binding.attachmentsList, false); composeAttachmentItemBinding.buttonPlay.setVisibility(View.GONE); String attachmentPath = attachment.local_path != null && !attachment.local_path.trim().isEmpty() ? attachment.local_path : attachment.preview_url; if (attachment.type != null || attachment.mimeType != null) { if ((attachment.type != null && attachment.type.toLowerCase().startsWith("image")) || (attachment.mimeType != null && attachment.mimeType.toLowerCase().startsWith("image"))) { Glide.with(composeAttachmentItemBinding.preview.getContext()) .load(attachmentPath) .into(composeAttachmentItemBinding.preview); } else if ((attachment.type != null && attachment.type.toLowerCase().startsWith("video")) || (attachment.mimeType != null && attachment.mimeType.toLowerCase().startsWith("video"))) { composeAttachmentItemBinding.buttonPlay.setVisibility(View.VISIBLE); long interval = 2000; RequestOptions options = new RequestOptions().frame(interval); Glide.with(composeAttachmentItemBinding.preview.getContext()).asBitmap() .load(attachmentPath) .apply(options) .into(composeAttachmentItemBinding.preview); } else if ((attachment.type != null && attachment.type.toLowerCase().startsWith("audio")) || (attachment.mimeType != null && attachment.mimeType.toLowerCase().startsWith("audio"))) { Glide.with(composeAttachmentItemBinding.preview.getContext()) .load(R.drawable.ic_baseline_audio_file_24) .into(composeAttachmentItemBinding.preview); } else { Glide.with(composeAttachmentItemBinding.preview.getContext()) .load(R.drawable.ic_baseline_insert_drive_file_24) .into(composeAttachmentItemBinding.preview); } } else { Glide.with(composeAttachmentItemBinding.preview.getContext()) .load(R.drawable.ic_baseline_insert_drive_file_24) .into(composeAttachmentItemBinding.preview); } if (mediaPosition == 0) { composeAttachmentItemBinding.buttonOrderUp.setVisibility(View.INVISIBLE); } else { composeAttachmentItemBinding.buttonOrderUp.setVisibility(View.VISIBLE); } if (mediaPosition == attachmentList.size() - 1) { composeAttachmentItemBinding.buttonOrderDown.setVisibility(View.INVISIBLE); } else { composeAttachmentItemBinding.buttonOrderDown.setVisibility(View.VISIBLE); } //Remote attachments when deleting/redrafting can't be ordered if (attachment.local_path == null) { composeAttachmentItemBinding.buttonOrderUp.setVisibility(View.INVISIBLE); composeAttachmentItemBinding.buttonOrderDown.setVisibility(View.INVISIBLE); } int finalMediaPosition = mediaPosition; composeAttachmentItemBinding.buttonDescription.setOnClickListener(v -> { AlertDialog.Builder builderInner = new AlertDialog.Builder(context, Helper.dialogStyle()); builderInner.setTitle(R.string.upload_form_description); PopupMediaDescriptionBinding popupMediaDescriptionBinding = PopupMediaDescriptionBinding.inflate(LayoutInflater.from(context), null, false); builderInner.setView(popupMediaDescriptionBinding.getRoot()); popupMediaDescriptionBinding.mediaDescription.setFilters(new InputFilter[]{new InputFilter.LengthFilter(1500)}); Glide.with(popupMediaDescriptionBinding.mediaPicture.getContext()) .asBitmap() .load(attachmentPath) .into(new CustomTarget() { @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) @Override public void onResourceReady(@NonNull Bitmap resource, Transition transition) { popupMediaDescriptionBinding.mediaPicture.setImageBitmap(resource); popupMediaDescriptionBinding.mediaPicture.setImageAlpha(60); } @Override public void onLoadCleared(@Nullable Drawable placeholder) { } }); builderInner.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); if (attachment.description != null) { popupMediaDescriptionBinding.mediaDescription.setText(attachment.description); popupMediaDescriptionBinding.mediaDescription.setSelection(popupMediaDescriptionBinding.mediaDescription.getText().length()); } builderInner.setPositiveButton(R.string.validate, (dialog, which) -> { attachment.description = popupMediaDescriptionBinding.mediaDescription.getText().toString(); displayAttachments(holder, position, finalMediaPosition); dialog.dismiss(); }); AlertDialog alertDialog = builderInner.create(); Objects.requireNonNull(alertDialog.getWindow()).setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); alertDialog.show(); popupMediaDescriptionBinding.mediaDescription.requestFocus(); }); composeAttachmentItemBinding.buttonOrderUp.setOnClickListener(v -> { if (finalMediaPosition > 0 && attachmentList.size() > 1) { Attachment at1 = attachmentList.get(finalMediaPosition); Attachment at2 = attachmentList.get(finalMediaPosition - 1); attachmentList.set(finalMediaPosition - 1, at1); attachmentList.set(finalMediaPosition, at2); displayAttachments(holder, position, finalMediaPosition - 1); } }); composeAttachmentItemBinding.buttonOrderDown.setOnClickListener(v -> { if (finalMediaPosition < (attachmentList.size() - 1) && attachmentList.size() > 1) { Attachment at1 = attachmentList.get(finalMediaPosition); Attachment at2 = attachmentList.get(finalMediaPosition + 1); attachmentList.set(finalMediaPosition, at2); attachmentList.set(finalMediaPosition + 1, at1); displayAttachments(holder, position, finalMediaPosition + 1); } }); composeAttachmentItemBinding.buttonRemove.setOnClickListener(v -> { AlertDialog.Builder builderInner = new AlertDialog.Builder(context, Helper.dialogStyle()); builderInner.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); builderInner.setPositiveButton(R.string.delete, (dialog, which) -> { attachmentList.remove(attachment); displayAttachments(holder, position, finalMediaPosition); new Thread(() -> { if (attachment.local_path != null) { File fileToDelete = new File(attachment.local_path); if (fileToDelete.exists()) { //noinspection ResultOfMethodCallIgnored fileToDelete.delete(); } } }).start(); }); builderInner.setMessage(R.string.toot_delete_media); builderInner.show(); }); composeAttachmentItemBinding.preview.setOnClickListener(v -> displayAttachments(holder, position, finalMediaPosition)); if (attachment.description == null || attachment.description.trim().isEmpty()) { ImageViewCompat.setImageTintList(composeAttachmentItemBinding.buttonDescription, ColorStateList.valueOf(ContextCompat.getColor(context, R.color.errorColor))); } else { ImageViewCompat.setImageTintList(composeAttachmentItemBinding.buttonDescription, ColorStateList.valueOf(ContextCompat.getColor(context, R.color.successColor))); } holder.binding.attachmentsList.addView(composeAttachmentItemBinding.getRoot()); mediaPosition++; } holder.binding.attachmentsList.setVisibility(View.VISIBLE); if (scrollToMediaPosition >= 0 && holder.binding.attachmentsList.getChildCount() < scrollToMediaPosition) { holder.binding.attachmentsList.requestChildFocus(holder.binding.attachmentsList.getChildAt(scrollToMediaPosition), holder.binding.attachmentsList.getChildAt(scrollToMediaPosition)); } } else { holder.binding.attachmentsList.setVisibility(View.GONE); holder.binding.sensitiveMedia.setVisibility(View.GONE); } } else { holder.binding.attachmentsList.setVisibility(View.GONE); holder.binding.sensitiveMedia.setVisibility(View.GONE); } buttonState(holder); } /** * Manage state of media and poll button * * @param holder ComposeViewHolder */ private void buttonState(ComposeViewHolder holder) { if (BaseMainActivity.software == null || BaseMainActivity.software.toUpperCase().compareTo("MASTODON") == 0) { if (holder.getAdapterPosition() > 0) { Status statusDraft = statusList.get(holder.getAdapterPosition()); if (statusDraft.poll == null) { holder.binding.buttonAttachImage.setEnabled(true); holder.binding.buttonAttachVideo.setEnabled(true); holder.binding.buttonAttachAudio.setEnabled(true); holder.binding.buttonAttachManual.setEnabled(true); } else { holder.binding.buttonAttachImage.setEnabled(false); holder.binding.buttonAttachVideo.setEnabled(false); holder.binding.buttonAttachAudio.setEnabled(false); holder.binding.buttonAttachManual.setEnabled(false); holder.binding.buttonPoll.setEnabled(true); } holder.binding.buttonPoll.setEnabled(statusDraft.media_attachments == null || statusDraft.media_attachments.size() <= 0); } } } public long getItemId(int position) { return position; } @Override public int getItemCount() { return statusList.size(); } /** * Initialize text watcher for content writing * It will allow to complete autocomplete edit text while starting words with @, #, : etc. * * @param holder {@link ComposeViewHolder} - current compose viewHolder * @return {@link TextWatcher} */ public TextWatcher initializeTextWatcher(ComposeAdapter.ComposeViewHolder holder) { final List[] emojis = new List[]{null}; String pattern = "(.|\\s)*(@[\\w_-]+@[a-z0-9.\\-]+|@[\\w_-]+)"; final Pattern mentionPattern = Pattern.compile(pattern); String patternTag = "^(.|\\s)*(#([\\w-]{2,}))$"; final Pattern tagPattern = Pattern.compile(patternTag); String patternEmoji = "^(.|\\s)*(:([\\w_]+))$"; final Pattern emojiPattern = Pattern.compile(patternEmoji); final int[] currentCursorPosition = {holder.binding.content.getSelectionStart()}; final String[] newContent = {null}; final int[] searchLength = {searchDeep}; TextWatcher textw; AccountsVM accountsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AccountsVM.class); SearchVM searchVM = new ViewModelProvider((ViewModelStoreOwner) context).get(SearchVM.class); textw = new TextWatcher() { private int position; @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 > 2) { holder.binding.addRemoveStatus.setVisibility(View.VISIBLE); } position = start; } @Override public void afterTextChanged(Editable s) { int currentLength = MastodonHelper.countLength(holder); //Copy/past if (currentLength > instanceInfo.configuration.statusesConf.max_characters + 1) { holder.binding.content.setText(s.delete(instanceInfo.configuration.statusesConf.max_characters - holder.binding.contentSpoiler.getText().length(), (currentLength - holder.binding.contentSpoiler.getText().length()))); } else if (currentLength > instanceInfo.configuration.statusesConf.max_characters) { holder.binding.content.setText(s.delete(position, position + 1)); } statusList.get(holder.getAdapterPosition()).text = s.toString(); if (s.toString().trim().length() < 2) { buttonVisibility(holder); } //Update cursor position statusList.get(holder.getAdapterPosition()).cursorPosition = holder.binding.content.getSelectionStart(); if (autocomplete) { holder.binding.content.removeTextChangedListener(this); Thread thread = new Thread() { @Override public void run() { String fedilabHugsTrigger = ":fedilab_hugs:"; String fedilabMorseTrigger = ":fedilab_morse:"; if (s.toString().contains(fedilabHugsTrigger)) { newContent[0] = s.toString().replaceAll(fedilabHugsTrigger, ""); int toFill = 500 - currentLength; if (toFill <= 0) { return; } StringBuilder hugs = new StringBuilder(); for (int i = 0; i < toFill; i++) { hugs.append(new String(Character.toChars(0x1F917))); } Handler mainHandler = new Handler(Looper.getMainLooper()); Runnable myRunnable = () -> { newContent[0] = newContent[0] + hugs; holder.binding.content.setText(newContent[0]); holder.binding.content.setSelection(holder.binding.content.getText().length()); autocomplete = false; updateCharacterCount(holder); }; mainHandler.post(myRunnable); } else if (s.toString().contains(fedilabMorseTrigger)) { newContent[0] = s.toString().replaceAll(fedilabMorseTrigger, "").trim(); List mentions = new ArrayList<>(); String mentionPattern = "@[a-z0-9_]+(@[a-z0-9.\\-]+[a-z0-9]+)?"; final Pattern mPattern = Pattern.compile(mentionPattern, Pattern.CASE_INSENSITIVE); Matcher matcherMentions = mPattern.matcher(newContent[0]); while (matcherMentions.find()) { mentions.add(matcherMentions.group()); } for (String mention : mentions) { newContent[0] = newContent[0].replace(mention, ""); } newContent[0] = Normalizer.normalize(newContent[0], Normalizer.Form.NFD); newContent[0] = newContent[0].replaceAll("[^\\p{ASCII}]", ""); HashMap ALPHA_TO_MORSE = new HashMap<>(); for (int i = 0; i < ALPHA.length && i < MORSE.length; i++) { ALPHA_TO_MORSE.put(ALPHA[i], MORSE[i]); } StringBuilder builder = new StringBuilder(); String[] words = newContent[0].trim().split(" "); for (String word : words) { for (int i = 0; i < word.length(); i++) { String morse = ALPHA_TO_MORSE.get(word.substring(i, i + 1).toLowerCase()); builder.append(morse).append(" "); } builder.append(" "); } newContent[0] = ""; for (String mention : mentions) { newContent[0] += mention + " "; } newContent[0] += builder.toString(); Handler mainHandler = new Handler(Looper.getMainLooper()); Runnable myRunnable = () -> { holder.binding.content.setText(newContent[0]); holder.binding.content.setSelection(holder.binding.content.getText().length()); autocomplete = false; updateCharacterCount(holder); }; mainHandler.post(myRunnable); } } }; thread.start(); return; } if (holder.binding.content.getSelectionStart() != 0) currentCursorPosition[0] = holder.binding.content.getSelectionStart(); if (s.toString().length() == 0) currentCursorPosition[0] = 0; //Only check last 15 characters before cursor position to avoid lags //Less than 15 characters are written before the cursor position searchLength[0] = Math.min(currentCursorPosition[0], searchDeep); if (currentCursorPosition[0] - (searchLength[0] - 1) < 0 || currentCursorPosition[0] == 0 || currentCursorPosition[0] > s.toString().length()) { updateCharacterCount(holder); return; } String patternh = "^(.|\\s)*(:fedilab_hugs:)$"; final Pattern hPattern = Pattern.compile(patternh); Matcher mh = hPattern.matcher((s.toString().substring(currentCursorPosition[0] - searchLength[0], currentCursorPosition[0]))); if (mh.matches()) { autocomplete = true; return; } String patternM = "^(.|\\s)*(:fedilab_morse:)$"; final Pattern mPattern = Pattern.compile(patternM); Matcher mm = mPattern.matcher((s.toString().substring(currentCursorPosition[0] - searchLength[0], currentCursorPosition[0]))); if (mm.matches()) { autocomplete = true; return; } String[] searchInArray = (s.toString().substring(currentCursorPosition[0] - searchLength[0], currentCursorPosition[0])).split("\\s"); if (searchInArray.length < 1) { updateCharacterCount(holder); return; } String searchIn = searchInArray[searchInArray.length - 1]; Matcher matcherMention, matcherTag, matcherEmoji; matcherMention = mentionPattern.matcher(searchIn); matcherTag = tagPattern.matcher(searchIn); matcherEmoji = emojiPattern.matcher(searchIn); if (matcherMention.matches()) { String searchGroup = matcherMention.group(); accountsVM.searchAccounts(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, searchGroup, 10, true, false).observe((LifecycleOwner) context, accounts -> { if (accounts == null) { return; } int currentCursorPosition = holder.binding.content.getSelectionStart(); AccountsSearchAdapter accountsListAdapter = new AccountsSearchAdapter(context, accounts); holder.binding.content.setThreshold(1); holder.binding.content.setAdapter(accountsListAdapter); final String oldContent = holder.binding.content.getText().toString(); if (oldContent.length() >= currentCursorPosition) { String[] searchA = oldContent.substring(0, currentCursorPosition).split("@"); if (searchA.length > 0) { final String search = searchA[searchA.length - 1]; holder.binding.content.setOnItemClickListener((parent, view, position, id) -> { app.fedilab.android.client.mastodon.entities.Account account = accounts.get(position); String deltaSearch = ""; int searchLength = searchDeep; if (currentCursorPosition < searchDeep) { //Less than 15 characters are written before the cursor position searchLength = currentCursorPosition; } if (currentCursorPosition - searchLength > 0 && currentCursorPosition < oldContent.length()) deltaSearch = oldContent.substring(currentCursorPosition - searchLength, currentCursorPosition); else { if (currentCursorPosition >= oldContent.length()) deltaSearch = oldContent.substring(currentCursorPosition - searchLength); } if (!search.equals("")) deltaSearch = deltaSearch.replace("@" + search, ""); String newContent = oldContent.substring(0, currentCursorPosition - searchLength); newContent += deltaSearch; newContent += "@" + account.acct + " "; int newPosition = newContent.length(); if (currentCursorPosition < oldContent.length()) newContent += oldContent.substring(currentCursorPosition); holder.binding.content.setText(newContent); updateCharacterCount(holder); holder.binding.content.setSelection(newPosition); AccountsSearchAdapter accountsListAdapter1 = new AccountsSearchAdapter(context, new ArrayList<>()); holder.binding.content.setThreshold(1); holder.binding.content.setAdapter(accountsListAdapter1); }); } } }); } else if (matcherTag.matches()) { String searchGroup = matcherTag.group(3); if (searchGroup != null) { searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, searchGroup, null, "hashtags", false, true, false, 0, null, null, 10).observe((LifecycleOwner) context, results -> { if (results == null) { return; } int currentCursorPosition = holder.binding.content.getSelectionStart(); TagsSearchAdapter tagsSearchAdapter = new TagsSearchAdapter(context, results.hashtags); holder.binding.content.setThreshold(1); holder.binding.content.setAdapter(tagsSearchAdapter); final String oldContent = holder.binding.content.getText().toString(); if (oldContent.length() < currentCursorPosition) return; String[] searchA = oldContent.substring(0, currentCursorPosition).split("#"); if (searchA.length < 1) return; final String search = searchA[searchA.length - 1]; holder.binding.content.setOnItemClickListener((parent, view, position, id) -> { if (position >= results.hashtags.size()) return; Tag tag = results.hashtags.get(position); String deltaSearch = ""; int searchLength = searchDeep; if (currentCursorPosition < searchDeep) { //Less than 15 characters are written before the cursor position searchLength = currentCursorPosition; } if (currentCursorPosition - searchLength > 0 && currentCursorPosition < oldContent.length()) deltaSearch = oldContent.substring(currentCursorPosition - searchLength, currentCursorPosition); else { if (currentCursorPosition >= oldContent.length()) deltaSearch = oldContent.substring(currentCursorPosition - searchLength); } if (!search.equals("")) deltaSearch = deltaSearch.replace("#" + search, ""); String newContent = oldContent.substring(0, currentCursorPosition - searchLength); newContent += deltaSearch; newContent += "#" + tag.name + " "; int newPosition = newContent.length(); if (currentCursorPosition < oldContent.length()) newContent += oldContent.substring(currentCursorPosition); holder.binding.content.setText(newContent); updateCharacterCount(holder); holder.binding.content.setSelection(newPosition); TagsSearchAdapter tagsSearchAdapter1 = new TagsSearchAdapter(context, new ArrayList<>()); holder.binding.content.setThreshold(1); holder.binding.content.setAdapter(tagsSearchAdapter1); }); }); } } else if (matcherEmoji.matches()) { String shortcode = matcherEmoji.group(3); new Thread(() -> { List emojisToDisplay = new ArrayList<>(); try { if (emojis[0] == null) { emojis[0] = new EmojiInstance(context).getEmojiList(BaseMainActivity.currentInstance); } if (emojis[0] == null) { return; } for (Emoji emoji : emojis[0]) { if (shortcode != null && emoji.shortcode.contains(shortcode)) { emojisToDisplay.add(emoji); if (emojisToDisplay.size() >= 10) { break; } } } Handler mainHandler = new Handler(Looper.getMainLooper()); Runnable myRunnable = () -> { int currentCursorPosition = holder.binding.content.getSelectionStart(); EmojiSearchAdapter emojisSearchAdapter = new EmojiSearchAdapter(context, emojisToDisplay); holder.binding.content.setThreshold(1); holder.binding.content.setAdapter(emojisSearchAdapter); final String oldContent = holder.binding.content.getText().toString(); String[] searchA = oldContent.substring(0, currentCursorPosition).split(":"); if (searchA.length > 0) { final String search = searchA[searchA.length - 1]; holder.binding.content.setOnItemClickListener((parent, view, position, id) -> { String shortcodeSelected = emojis[0].get(position).shortcode; String deltaSearch = ""; int searchLength = searchDeep; if (currentCursorPosition < searchDeep) { //Less than 15 characters are written before the cursor position searchLength = currentCursorPosition; } if (currentCursorPosition - searchLength > 0 && currentCursorPosition < oldContent.length()) deltaSearch = oldContent.substring(currentCursorPosition - searchLength, currentCursorPosition); else { if (currentCursorPosition >= oldContent.length()) deltaSearch = oldContent.substring(currentCursorPosition - searchLength); } if (!search.equals("")) deltaSearch = deltaSearch.replace(":" + search, ""); String newContent = oldContent.substring(0, currentCursorPosition - searchLength); newContent += deltaSearch; newContent += ":" + shortcodeSelected + ": "; int newPosition = newContent.length(); if (currentCursorPosition < oldContent.length()) newContent += oldContent.substring(currentCursorPosition); holder.binding.content.setText(newContent); updateCharacterCount(holder); holder.binding.content.setSelection(newPosition); EmojiSearchAdapter emojisSearchAdapter1 = new EmojiSearchAdapter(context, new ArrayList<>()); holder.binding.content.setThreshold(1); holder.binding.content.setAdapter(emojisSearchAdapter1); }); } }; mainHandler.post(myRunnable); } catch (DBException e) { e.printStackTrace(); } }).start(); } else { holder.binding.content.dismissDropDown(); } updateCharacterCount(holder); } }; return textw; } @SuppressLint("ClickableViewAccessibility") @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { if (getItemViewType(position) == TYPE_NORMAL) { Status status = statusList.get(position); StatusSimpleViewHolder holder = (StatusSimpleViewHolder) viewHolder; holder.binding.statusContent.setText(status.span_content, TextView.BufferType.SPANNABLE); MastodonHelper.loadPPMastodon(holder.binding.avatar, status.account); holder.binding.displayName.setText(status.account.span_display_name, TextView.BufferType.SPANNABLE); holder.binding.username.setText(String.format("@%s", status.account.acct)); if (status.spoiler_text != null && !status.spoiler_text.trim().isEmpty()) { holder.binding.spoiler.setVisibility(View.VISIBLE); holder.binding.spoiler.setText(status.span_spoiler_text, TextView.BufferType.SPANNABLE); } else { holder.binding.spoiler.setVisibility(View.GONE); holder.binding.spoiler.setText(null); } } else if (getItemViewType(position) == TYPE_COMPOSE) { Status statusDraft = statusList.get(position); //Fill emoji and instance info if (emojis == null) { new Thread(() -> { try { emojis = new EmojiInstance(context).getEmojiList(BaseMainActivity.currentInstance); } catch (DBException e) { e.printStackTrace(); } }).start(); } ComposeViewHolder holder = (ComposeViewHolder) viewHolder; holder.binding.buttonAttach.setOnClickListener(v -> { if (instanceInfo.configuration.media_attachments.supported_mime_types != null) { if (instanceInfo.getMimeTypeAudio().size() == 0) { holder.binding.buttonAttachAudio.setEnabled(false); } if (instanceInfo.getMimeTypeImage().size() == 0) { holder.binding.buttonAttachImage.setEnabled(false); } if (instanceInfo.getMimeTypeVideo().size() == 0) { holder.binding.buttonAttachVideo.setEnabled(false); } if (instanceInfo.getMimeTypeOther().size() == 0) { holder.binding.buttonAttachManual.setEnabled(false); } } holder.binding.attachmentChoicesPanel.setVisibility(View.VISIBLE); }); //Disable buttons to attach media if max has been reached if (statusDraft.media_attachments != null && statusDraft.media_attachments.size() >= instanceInfo.configuration.statusesConf.max_media_attachments) { holder.binding.buttonAttachImage.setEnabled(false); holder.binding.buttonAttachVideo.setEnabled(false); holder.binding.buttonAttachAudio.setEnabled(false); holder.binding.buttonAttachManual.setEnabled(false); } else { holder.binding.buttonAttachImage.setEnabled(true); holder.binding.buttonAttachVideo.setEnabled(true); holder.binding.buttonAttachAudio.setEnabled(true); holder.binding.buttonAttachManual.setEnabled(true); } holder.binding.buttonAttachAudio.setOnClickListener(v -> { holder.binding.attachmentChoicesPanel.setVisibility(View.GONE); pickupMedia(ComposeActivity.mediaType.AUDIO, position); }); holder.binding.buttonAttachImage.setOnClickListener(v -> { holder.binding.attachmentChoicesPanel.setVisibility(View.GONE); pickupMedia(ComposeActivity.mediaType.PHOTO, position); }); holder.binding.buttonAttachVideo.setOnClickListener(v -> { holder.binding.attachmentChoicesPanel.setVisibility(View.GONE); pickupMedia(ComposeActivity.mediaType.VIDEO, position); }); holder.binding.buttonAttachManual.setOnClickListener(v -> { holder.binding.attachmentChoicesPanel.setVisibility(View.GONE); pickupMedia(ComposeActivity.mediaType.ALL, position); }); //Used for DM if (visibility != null) { statusDraft.visibility = visibility; } if (statusDraft.visibility == null) { if (position > 0) { statusDraft.visibility = statusList.get(position - 1).visibility; } else if (BaseMainActivity.accountWeakReference.get().mastodon_account != null) { statusDraft.visibility = BaseMainActivity.accountWeakReference.get().mastodon_account.source.privacy; } else { statusDraft.visibility = "public"; } } switch (statusDraft.visibility.toLowerCase()) { case "public": holder.binding.buttonVisibility.setImageResource(R.drawable.ic_compose_visibility_public); statusDraft.visibility = MastodonHelper.visibility.PUBLIC.name(); break; case "unlisted": holder.binding.buttonVisibility.setImageResource(R.drawable.ic_compose_visibility_unlisted); statusDraft.visibility = MastodonHelper.visibility.UNLISTED.name(); break; case "private": holder.binding.buttonVisibility.setImageResource(R.drawable.ic_compose_visibility_private); statusDraft.visibility = MastodonHelper.visibility.PRIVATE.name(); break; case "direct": holder.binding.buttonVisibility.setImageResource(R.drawable.ic_compose_visibility_direct); statusDraft.visibility = MastodonHelper.visibility.DIRECT.name(); break; } holder.binding.buttonCloseAttachmentPanel.setOnClickListener(v -> holder.binding.attachmentChoicesPanel.setVisibility(View.GONE)); holder.binding.buttonVisibility.setOnClickListener(v -> holder.binding.visibilityPanel.setVisibility(View.VISIBLE)); holder.binding.buttonCloseVisibilityPanel.setOnClickListener(v -> holder.binding.visibilityPanel.setVisibility(View.GONE)); holder.binding.buttonVisibilityDirect.setOnClickListener(v -> { holder.binding.visibilityPanel.setVisibility(View.GONE); holder.binding.buttonVisibility.setImageResource(R.drawable.ic_compose_visibility_direct); statusDraft.visibility = MastodonHelper.visibility.DIRECT.name(); }); holder.binding.buttonVisibilityPrivate.setOnClickListener(v -> { holder.binding.visibilityPanel.setVisibility(View.GONE); holder.binding.buttonVisibility.setImageResource(R.drawable.ic_compose_visibility_private); statusDraft.visibility = MastodonHelper.visibility.PRIVATE.name(); }); holder.binding.buttonVisibilityUnlisted.setOnClickListener(v -> { holder.binding.visibilityPanel.setVisibility(View.GONE); holder.binding.buttonVisibility.setImageResource(R.drawable.ic_compose_visibility_unlisted); statusDraft.visibility = MastodonHelper.visibility.UNLISTED.name(); }); holder.binding.buttonVisibilityPublic.setOnClickListener(v -> { holder.binding.visibilityPanel.setVisibility(View.GONE); holder.binding.buttonVisibility.setImageResource(R.drawable.ic_compose_visibility_public); statusDraft.visibility = MastodonHelper.visibility.PUBLIC.name(); }); holder.binding.buttonSensitive.setOnClickListener(v -> { if (holder.binding.contentSpoiler.getVisibility() == View.VISIBLE) holder.binding.contentSpoiler.setVisibility(View.GONE); else holder.binding.contentSpoiler.setVisibility(View.VISIBLE); }); //Last compose drawer buttonVisibility(holder); holder.binding.buttonEmoji.setOnClickListener(v -> { try { displayEmojiPicker(holder); } catch (DBException e) { e.printStackTrace(); } }); displayAttachments(holder, position, -1); manageMentions(context, statusDraft, holder); //For some instances this value can be null, we have to transform the html content if (statusDraft.text == null && statusDraft.content != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) statusDraft.text = new SpannableString(Html.fromHtml(statusDraft.content, Html.FROM_HTML_MODE_LEGACY)).toString(); else statusDraft.text = new SpannableString(Html.fromHtml(statusDraft.content)).toString(); } holder.binding.content.setText(statusDraft.text); holder.binding.content.setSelection(statusDraft.cursorPosition); if (statusDraft.setCursorToEnd) { statusDraft.setCursorToEnd = false; holder.binding.content.setSelection(holder.binding.content.getText().length()); } if (statusDraft.spoiler_text != null) { holder.binding.contentSpoiler.setText(statusDraft.spoiler_text); holder.binding.contentSpoiler.setSelection(holder.binding.contentSpoiler.getText().length()); } else { holder.binding.contentSpoiler.setText(""); } holder.binding.sensitiveMedia.setChecked(statusDraft.sensitive); holder.binding.content.addTextChangedListener(initializeTextWatcher(holder)); holder.binding.buttonPoll.setOnClickListener(v -> displayPollPopup(holder, statusDraft, position)); holder.binding.buttonPoll.setOnClickListener(v -> displayPollPopup(holder, statusDraft, position)); holder.binding.characterProgress.setMax(instanceInfo.configuration.statusesConf.max_characters); holder.binding.contentSpoiler.addTextChangedListener(new TextWatcher() { private int position; @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { position = start; if (count > 2) { holder.binding.addRemoveStatus.setVisibility(View.VISIBLE); } } @Override public void afterTextChanged(Editable s) { int currentLength = MastodonHelper.countLength(holder); if (currentLength > instanceInfo.configuration.statusesConf.max_characters + 1) { holder.binding.contentSpoiler.setText(s.delete(instanceInfo.configuration.statusesConf.max_characters - holder.binding.content.getText().length(), (currentLength - holder.binding.content.getText().length()))); buttonVisibility(holder); } else if (currentLength > instanceInfo.configuration.statusesConf.max_characters) { buttonVisibility(holder); holder.binding.contentSpoiler.setText(s.delete(position, position + 1)); } statusList.get(holder.getAdapterPosition()).spoiler_text = s.toString(); if (s.toString().trim().length() < 2) { buttonVisibility(holder); } updateCharacterCount(holder); } }); if (statusDraft.poll != null) { ImageViewCompat.setImageTintList(holder.binding.buttonPoll, ColorStateList.valueOf(ContextCompat.getColor(context, R.color.cyanea_accent))); } else { ImageViewCompat.setImageTintList(holder.binding.buttonPoll, null); } holder.binding.buttonPost.setOnClickListener(v -> manageDrafts.onSubmit(prepareDraft(statusList, this, account.instance, account.user_id))); } } private void displayEmojiPicker(ComposeViewHolder holder) throws DBException { if (emojis != null) { emojis.clear(); emojis = null; } emojis = new EmojiInstance(context).getEmojiList(BaseMainActivity.currentInstance); final AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle()); int paddingPixel = 15; float density = context.getResources().getDisplayMetrics().density; int paddingDp = (int) (paddingPixel * density); builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); builder.setTitle(R.string.insert_emoji); if (emojis != null && emojis.size() > 0) { GridView gridView = new GridView(context); gridView.setAdapter(new EmojiAdapter(emojis)); gridView.setNumColumns(5); gridView.setOnItemClickListener((parent, view, position, id) -> { holder.binding.content.getText().insert(holder.binding.content.getSelectionStart(), " :" + emojis.get(position).shortcode + ": "); alertDialogEmoji.dismiss(); }); gridView.setPadding(paddingDp, paddingDp, paddingDp, paddingDp); builder.setView(gridView); } else { TextView textView = new TextView(context); textView.setText(context.getString(R.string.no_emoji)); textView.setPadding(paddingDp, paddingDp, paddingDp, paddingDp); builder.setView(textView); } alertDialogEmoji = builder.show(); } private void displayPollPopup(ComposeAdapter.ComposeViewHolder holder, Status statusDraft, int position) { AlertDialog.Builder alertPoll = new AlertDialog.Builder(context, Helper.dialogStyle()); alertPoll.setTitle(R.string.create_poll); ComposePollBinding composePollBinding = ComposePollBinding.inflate(LayoutInflater.from(context), new LinearLayout(context), false); alertPoll.setView(composePollBinding.getRoot()); int max_entry = 4; int max_length = 25; final int[] pollCountItem = {2}; if (instanceInfo != null && instanceInfo.configuration != null && instanceInfo.configuration.pollsConf != null) { max_entry = instanceInfo.configuration.pollsConf.max_options; max_length = instanceInfo.configuration.pollsConf.max_option_chars; } InputFilter[] fArray = new InputFilter[1]; fArray[0] = new InputFilter.LengthFilter(max_length); composePollBinding.option1.text.setFilters(fArray); composePollBinding.option1.text.setHint(context.getString(R.string.poll_choice_s, 1)); composePollBinding.option2.text.setFilters(fArray); composePollBinding.option2.text.setHint(context.getString(R.string.poll_choice_s, 2)); composePollBinding.option1.buttonRemove.setVisibility(View.GONE); composePollBinding.option2.buttonRemove.setVisibility(View.GONE); int finalMax_entry = max_entry; composePollBinding.buttonAddOption.setOnClickListener(v -> { if (pollCountItem[0] < finalMax_entry) { ComposePollItemBinding composePollItemBinding = ComposePollItemBinding.inflate(LayoutInflater.from(context), new LinearLayout(context), false); composePollItemBinding.text.setFilters(fArray); composePollItemBinding.text.setHint(context.getString(R.string.poll_choice_s, (pollCountItem[0] + 1))); LinearLayoutCompat viewItem = composePollItemBinding.getRoot(); composePollBinding.optionsList.addView(composePollItemBinding.getRoot()); composePollItemBinding.buttonRemove.setOnClickListener(view -> { composePollBinding.optionsList.removeView(viewItem); pollCountItem[0]--; if (pollCountItem[0] >= finalMax_entry) { composePollBinding.buttonAddOption.setVisibility(View.GONE); } else { composePollBinding.buttonAddOption.setVisibility(View.VISIBLE); } int childCount = composePollBinding.optionsList.getChildCount(); for (int i = 0; i < childCount; i++) { AppCompatEditText title = (composePollBinding.optionsList.getChildAt(i)).findViewById(R.id.text); title.setHint(context.getString(R.string.poll_choice_s, i + 1)); } }); } pollCountItem[0]++; if (pollCountItem[0] >= finalMax_entry) { composePollBinding.buttonAddOption.setVisibility(View.GONE); } else { composePollBinding.buttonAddOption.setVisibility(View.VISIBLE); } }); ArrayAdapter pollduration = ArrayAdapter.createFromResource(context, R.array.poll_duration, android.R.layout.simple_spinner_dropdown_item); ArrayAdapter pollchoice = ArrayAdapter.createFromResource(context, R.array.poll_choice_type, android.R.layout.simple_spinner_dropdown_item); composePollBinding.pollType.setAdapter(pollchoice); composePollBinding.pollDuration.setAdapter(pollduration); composePollBinding.pollDuration.setSelection(4); composePollBinding.pollType.setSelection(0); if (statusDraft != null && statusDraft.poll != null && statusDraft.poll.options != null) { int i = 1; for (Poll.PollItem pollItem : statusDraft.poll.options) { if (i == 1) { if (pollItem.title != null) composePollBinding.option1.text.setText(pollItem.title); } else if (i == 2) { if (pollItem.title != null) composePollBinding.option2.text.setText(pollItem.title); } else { ComposePollItemBinding composePollItemBinding = ComposePollItemBinding.inflate(LayoutInflater.from(context), new LinearLayout(context), false); composePollItemBinding.text.setFilters(fArray); composePollItemBinding.text.setHint(context.getString(R.string.poll_choice_s, (pollCountItem[0] + 1))); composePollItemBinding.text.setText(pollItem.title); composePollBinding.optionsList.addView(composePollItemBinding.getRoot()); composePollItemBinding.buttonRemove.setOnClickListener(view -> { composePollBinding.optionsList.removeView(view); pollCountItem[0]--; }); pollCountItem[0]++; } i++; } if (statusDraft.poll.options.size() >= max_entry) { composePollBinding.buttonAddOption.setVisibility(View.GONE); } switch (statusDraft.poll.expire_in) { case 300: composePollBinding.pollDuration.setSelection(0); break; case 1800: composePollBinding.pollDuration.setSelection(1); break; case 3600: composePollBinding.pollDuration.setSelection(2); break; case 21600: composePollBinding.pollDuration.setSelection(3); break; case 86400: composePollBinding.pollDuration.setSelection(4); break; case 259200: composePollBinding.pollDuration.setSelection(5); break; case 604800: composePollBinding.pollDuration.setSelection(6); break; } if (statusDraft.poll.multiple) composePollBinding.pollType.setSelection(1); else composePollBinding.pollType.setSelection(0); } alertPoll.setNegativeButton(R.string.delete, (dialog, whichButton) -> { if (statusDraft != null && statusDraft.poll != null) statusDraft.poll = null; buttonState(holder); dialog.dismiss(); notifyItemChanged(position); }); alertPoll.setPositiveButton(R.string.validate, null); final AlertDialog alertPollDiaslog = alertPoll.create(); alertPollDiaslog.setOnShowListener(dialog -> { Button b = alertPollDiaslog.getButton(AlertDialog.BUTTON_POSITIVE); b.setOnClickListener(view1 -> { int poll_duration_pos = composePollBinding.pollDuration.getSelectedItemPosition(); int poll_choice_pos = composePollBinding.pollType.getSelectedItemPosition(); String choice1 = composePollBinding.option1.text.getText().toString().trim(); String choice2 = composePollBinding.option2.text.getText().toString().trim(); if (choice1.isEmpty() && choice2.isEmpty()) { Toasty.error(context, context.getString(R.string.poll_invalid_choices), Toasty.LENGTH_SHORT).show(); } else if (statusDraft != null) { statusDraft.poll = new Poll(); statusDraft.poll.multiple = (poll_choice_pos != 0); int expire; switch (poll_duration_pos) { case 0: expire = 300; break; case 1: expire = 1800; break; case 2: expire = 3600; break; case 3: expire = 21600; break; case 4: expire = 86400; break; case 5: expire = 259200; break; case 6: expire = 604800; break; default: expire = 864000; } statusDraft.poll.expire_in = expire; List pollItems = new ArrayList<>(); int childCount = composePollBinding.optionsList.getChildCount(); for (int i = 0; i < childCount; i++) { Poll.PollItem pollItem = new Poll.PollItem(); AppCompatEditText title = (composePollBinding.optionsList.getChildAt(i)).findViewById(R.id.text); pollItem.title = title.getText().toString(); pollItems.add(pollItem); } List options = new ArrayList<>(); boolean doubleTitle = false; for (Poll.PollItem po : pollItems) { if (!options.contains(po.title.trim())) { options.add(po.title.trim()); } else { doubleTitle = true; } } if (!doubleTitle) { statusDraft.poll.options = pollItems; dialog.dismiss(); } else { Toasty.error(context, context.getString(R.string.poll_duplicated_entry), Toasty.LENGTH_SHORT).show(); } } holder.binding.buttonPoll.setVisibility(View.VISIBLE); buttonState(holder); notifyItemChanged(position); }); }); alertPollDiaslog.show(); } public interface ManageDrafts { void onItemDraftAdded(int position); void onItemDraftDeleted(Status status, int position); void onSubmit(StatusDraft statusDraft); } public static class StatusSimpleViewHolder extends RecyclerView.ViewHolder { DrawerStatusSimpleBinding binding; StatusSimpleViewHolder(DrawerStatusSimpleBinding itemView) { super(itemView.getRoot()); binding = itemView; } } public static class ComposeViewHolder extends RecyclerView.ViewHolder { public DrawerStatusComposeBinding binding; ComposeViewHolder(DrawerStatusComposeBinding itemView) { super(itemView.getRoot()); binding = itemView; } } }