diff options
author | Thomas <tschneider.ac@gmail.com> | 2023-12-14 16:55:49 +0100 |
---|---|---|
committer | Thomas <tschneider.ac@gmail.com> | 2023-12-14 16:55:49 +0100 |
commit | bfc362ef6225d1764801378c5923e67d9ca5e5f3 (patch) | |
tree | d01722207b1c3078ab928a43e9c2813a2c657bb7 | |
parent | f5bfb3e250f2161772e268a229d534932a8f188b (diff) |
Automatically split long copied/pasted messages into threads
3 files changed, 182 insertions, 84 deletions
diff --git a/app/src/main/java/app/fedilab/android/mastodon/activities/ComposeActivity.java b/app/src/main/java/app/fedilab/android/mastodon/activities/ComposeActivity.java index a1ce331f0..cf9c72f5c 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/activities/ComposeActivity.java +++ b/app/src/main/java/app/fedilab/android/mastodon/activities/ComposeActivity.java @@ -774,12 +774,15 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana } @Override - public void onItemDraftAdded(int position) { + 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) { diff --git a/app/src/main/java/app/fedilab/android/mastodon/helper/ComposeHelper.java b/app/src/main/java/app/fedilab/android/mastodon/helper/ComposeHelper.java new file mode 100644 index 000000000..faec45de5 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/mastodon/helper/ComposeHelper.java @@ -0,0 +1,98 @@ +package app.fedilab.android.mastodon.helper; +/* Copyright 2023 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 <http://www.gnu.org/licenses>. */ + +import static app.fedilab.android.mastodon.helper.Helper.mentionLongPattern; +import static app.fedilab.android.mastodon.helper.Helper.mentionPattern; + +import java.util.ArrayList; +import java.util.regex.Matcher; + +public class ComposeHelper { + + + /** + * Allows to split the toot by dot "." for sentences - adds number at the end automatically + * + * @param content String initial content + * @param maxChars int the max chars per toot + * @return ArrayList<String> split toot + */ + public static ArrayList<String> splitToots(String content, int maxChars) { + String[] splitContent = content.split("\\s"); + + + ArrayList<String> mentions = new ArrayList<>(); + int mentionLength = 0; + StringBuilder mentionString = new StringBuilder(); + Matcher matcher = mentionLongPattern.matcher(content); + while (matcher.find()) { + String mentionLong = matcher.group(1); + if (!mentions.contains(mentionLong)) { + mentions.add(mentionLong); + } + } + matcher = mentionPattern.matcher(content); + while (matcher.find()) { + String mention = matcher.group(1); + if (!mentions.contains(mention)) { + mentions.add(mention); + } + } + for (String mention : mentions) { + mentionString.append(mention).append(" "); + } + mentionLength = mentionString.length() + 1; + int maxCharsPerMessage = maxChars - mentionLength; + int totalCurrent = 0; + ArrayList<String> reply = new ArrayList<>(); + int index = 0; + for (String s : splitContent) { + if ((totalCurrent + s.length() + 1) < maxCharsPerMessage) { + totalCurrent += (s.length() + 1); + } else { + if (content.length() > totalCurrent && totalCurrent > 0) { + String tempContent = content.substring(0, (totalCurrent)); + content = content.substring(totalCurrent); + + reply.add(index, tempContent); + index++; + totalCurrent = s.length() + 1; + } + } + } + if (totalCurrent > 0) { + reply.add(index, content); + } + if (reply.size() > 1) { + int i = 0; + for (String r : reply) { + if (mentions.size() > 0) { + String tmpMention = mentionString.toString(); + for (String mention : mentions) { + if (r.contains(mention)) { + tmpMention = tmpMention.replace(mention, ""); + } + } + reply.set(i, r + " " + tmpMention); + } else { + reply.set(i, r); + } + i++; + } + } + return reply; + } +} diff --git a/app/src/main/java/app/fedilab/android/mastodon/ui/drawer/ComposeAdapter.java b/app/src/main/java/app/fedilab/android/mastodon/ui/drawer/ComposeAdapter.java index ce1227e56..ab73f1a02 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/ui/drawer/ComposeAdapter.java +++ b/app/src/main/java/app/fedilab/android/mastodon/ui/drawer/ComposeAdapter.java @@ -89,6 +89,7 @@ import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.Random; import java.util.Set; import java.util.regex.Matcher; @@ -117,6 +118,7 @@ import app.fedilab.android.mastodon.client.entities.app.Languages; import app.fedilab.android.mastodon.client.entities.app.Quotes; import app.fedilab.android.mastodon.client.entities.app.StatusDraft; import app.fedilab.android.mastodon.exception.DBException; +import app.fedilab.android.mastodon.helper.ComposeHelper; import app.fedilab.android.mastodon.helper.Helper; import app.fedilab.android.mastodon.helper.MastodonHelper; import app.fedilab.android.mastodon.helper.ThemeHelper; @@ -467,7 +469,7 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder holder.binding.addRemoveStatus.setIconResource(R.drawable.ic_compose_thread_add_status); holder.binding.addRemoveStatus.setContentDescription(context.getString(R.string.add_status)); holder.binding.addRemoveStatus.setOnClickListener(v -> { - manageDrafts.onItemDraftAdded(statusList.size()); + manageDrafts.onItemDraftAdded(statusList.size(), null); buttonVisibility(holder); }); } @@ -475,7 +477,7 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder holder.binding.addRemoveStatus.setIconResource(R.drawable.ic_compose_thread_add_status); holder.binding.addRemoveStatus.setContentDescription(context.getString(R.string.add_status)); holder.binding.addRemoveStatus.setOnClickListener(v -> { - manageDrafts.onItemDraftAdded(statusList.size()); + manageDrafts.onItemDraftAdded(statusList.size(), null); buttonVisibility(holder); }); } @@ -523,6 +525,7 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder TextWatcher textw; AccountsVM accountsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AccountsVM.class); SearchVM searchVM = new ViewModelProvider((ViewModelStoreOwner) context).get(SearchVM.class); + final boolean[] proceedToSplit = {false}; textw = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { @@ -531,10 +534,33 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder @Override public void onTextChanged(CharSequence s, int start, int before, int count) { buttonVisibility(holder); + //Text is copied pasted and the content is greater than the max of the instance + int max_car = MastodonHelper.getInstanceMaxChars(context); + if (count > max_car) { + proceedToSplit[0] = true; + ArrayList<String> splitText = ComposeHelper.splitToots(s.toString(), max_car); + int statusListSize = statusList.size(); + int i = 0; + for(String message: splitText) { + if(i==0) { + i++; + continue; + } + manageDrafts.onItemDraftAdded(statusListSize+(i-1), message); + buttonVisibility(holder); + i++; + } + } } @Override public void afterTextChanged(Editable s) { + String contentString = s.toString(); + if(proceedToSplit[0]) { + int max_car = MastodonHelper.getInstanceMaxChars(context); + ArrayList<String> splitText = ComposeHelper.splitToots(contentString, max_car); + contentString = splitText.get(0); + } int currentLength = MastodonHelper.countLength(holder); if (promptDraftListener != null) { promptDraftListener.promptDraft(); @@ -553,22 +579,23 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder if (holder.getBindingAdapterPosition() < 0) { return; } - statusList.get(holder.getBindingAdapterPosition()).text = s.toString(); - if (s.toString().trim().length() < 2) { + statusList.get(holder.getBindingAdapterPosition()).text = contentString; + if (contentString.trim().length() < 2) { buttonVisibility(holder); } //Update cursor position //statusList.get(holder.getBindingAdapterPosition()).cursorPosition = holder.binding.content.getSelectionStart(); if (autocomplete) { holder.binding.content.removeTextChangedListener(this); + String finalContentString = contentString; Thread thread = new Thread() { @Override public void run() { String fedilabHugsTrigger = ":fedilab_hugs:"; String fedilabMorseTrigger = ":fedilab_morse:"; String fedilabQuoteTrigger = ":fedilab_quote:"; - if (s.toString().contains(fedilabHugsTrigger)) { - newContent[0] = s.toString().replaceAll(Pattern.quote(fedilabHugsTrigger), "").trim(); + if (finalContentString.contains(fedilabHugsTrigger)) { + newContent[0] = finalContentString.replaceAll(Pattern.quote(fedilabHugsTrigger), "").trim(); int toFill = 500 - newContent[0].length(); if (toFill <= 0) { return; @@ -589,8 +616,8 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder updateCharacterCount(holder); }; mainHandler.post(myRunnable); - } else if (s.toString().contains(fedilabMorseTrigger)) { - newContent[0] = s.toString().replaceAll(fedilabMorseTrigger, "").trim(); + } else if (finalContentString.contains(fedilabMorseTrigger)) { + newContent[0] = finalContentString.replaceAll(fedilabMorseTrigger, "").trim(); List<String> mentions = new ArrayList<>(); String mentionPattern = "@[a-z0-9_]+(@[a-z0-9.\\-]+[a-z0-9]+)?"; final Pattern mPattern = Pattern.compile(mentionPattern, Pattern.CASE_INSENSITIVE); @@ -634,8 +661,8 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder updateCharacterCount(holder); }; mainHandler.post(myRunnable); - } else if (s.toString().contains(fedilabQuoteTrigger)) { - newContent[0] = s.toString().replaceAll(fedilabQuoteTrigger, "").trim(); + } else if (finalContentString.contains(fedilabQuoteTrigger)) { + newContent[0] = finalContentString.replaceAll(fedilabQuoteTrigger, "").trim(); List<String> mentions = new ArrayList<>(); String mentionPattern = "@[a-z0-9_]+(@[a-z0-9.\\-]+[a-z0-9]+)?"; final Pattern mPattern = Pattern.compile(mentionPattern, Pattern.CASE_INSENSITIVE); @@ -694,37 +721,35 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder if (holder.binding.content.getSelectionStart() != 0) currentCursorPosition[0] = holder.binding.content.getSelectionStart(); - if (s.toString().length() == 0) + if (contentString.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()) { + if (currentCursorPosition[0] - (searchLength[0] - 1) < 0 || currentCursorPosition[0] == 0 || currentCursorPosition[0] > contentString.length()) { updateCharacterCount(holder); return; } - Matcher mathsPatterns = Helper.mathsComposePattern.matcher((s.toString())); + Matcher mathsPatterns = Helper.mathsComposePattern.matcher((contentString)); if (mathsPatterns.find()) { if (holder.binding.laTexViewContainer.getChildCount() == 0) { MathJaxConfig mathJaxConfig = new MathJaxConfig(); mathJaxConfig.setAutomaticLinebreaks(true); mathJaxConfig.setInput(TeX); switch (context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) { - case Configuration.UI_MODE_NIGHT_YES: - mathJaxConfig.setTextColor("white"); - break; - case Configuration.UI_MODE_NIGHT_NO: - mathJaxConfig.setTextColor("black"); - break; + case Configuration.UI_MODE_NIGHT_YES -> + mathJaxConfig.setTextColor("white"); + case Configuration.UI_MODE_NIGHT_NO -> + mathJaxConfig.setTextColor("black"); } statusList.get(holder.getBindingAdapterPosition()).mathJaxView = new MathJaxView(context, mathJaxConfig); holder.binding.laTexViewContainer.addView(statusList.get(holder.getBindingAdapterPosition()).mathJaxView); holder.binding.laTexViewContainer.setVisibility(View.VISIBLE); } if (statusList.get(holder.getBindingAdapterPosition()).mathJaxView != null) { - statusList.get(holder.getBindingAdapterPosition()).mathJaxView.setInputText(s.toString()); + statusList.get(holder.getBindingAdapterPosition()).mathJaxView.setInputText(contentString); } } else { @@ -733,7 +758,7 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder String patternh = "^(.|\\s)*(:fedilab_hugs:)"; final Pattern hPattern = Pattern.compile(patternh); - Matcher mh = hPattern.matcher((s.toString().substring(currentCursorPosition[0] - searchLength[0], currentCursorPosition[0]))); + Matcher mh = hPattern.matcher((contentString.substring(currentCursorPosition[0] - searchLength[0], currentCursorPosition[0]))); if (mh.matches()) { autocomplete = true; @@ -742,7 +767,7 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder String patternM = "^(.|\\s)*(:fedilab_morse:)"; final Pattern mPattern = Pattern.compile(patternM); - Matcher mm = mPattern.matcher((s.toString().substring(currentCursorPosition[0] - searchLength[0], currentCursorPosition[0]))); + Matcher mm = mPattern.matcher((contentString.substring(currentCursorPosition[0] - searchLength[0], currentCursorPosition[0]))); if (mm.matches()) { autocomplete = true; return; @@ -750,13 +775,13 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder String patternQ = "^(.|\\s)*(:fedilab_quote:)"; final Pattern qPattern = Pattern.compile(patternQ); - Matcher mq = qPattern.matcher((s.toString().substring(currentCursorPosition[0] - searchLength[0], currentCursorPosition[0]))); + Matcher mq = qPattern.matcher((contentString.substring(currentCursorPosition[0] - searchLength[0], currentCursorPosition[0]))); if (mq.matches()) { autocomplete = true; return; } - String[] searchInArray = (s.toString().substring(currentCursorPosition[0] - searchLength[0], currentCursorPosition[0])).split("\\s"); + String[] searchInArray = (contentString.substring(currentCursorPosition[0] - searchLength[0], currentCursorPosition[0])).split("\\s"); if (searchInArray.length < 1) { updateCharacterCount(holder); return; @@ -1498,22 +1523,22 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder } switch (statusDraft.visibility.toLowerCase()) { - case "public": + case "public" -> { holder.binding.buttonVisibility.setIconResource(R.drawable.ic_compose_visibility_public); statusDraft.visibility = MastodonHelper.visibility.PUBLIC.name(); - break; - case "unlisted": + } + case "unlisted" -> { holder.binding.buttonVisibility.setIconResource(R.drawable.ic_compose_visibility_unlisted); statusDraft.visibility = MastodonHelper.visibility.UNLISTED.name(); - break; - case "private": + } + case "private" -> { holder.binding.buttonVisibility.setIconResource(R.drawable.ic_compose_visibility_private); statusDraft.visibility = MastodonHelper.visibility.PRIVATE.name(); - break; - case "direct": + } + case "direct" -> { holder.binding.buttonVisibility.setIconResource(R.drawable.ic_compose_visibility_direct); statusDraft.visibility = MastodonHelper.visibility.DIRECT.name(); - break; + } } holder.binding.visibilityPanel.setOnTouchListener((view, motionEvent) -> true); @@ -1582,7 +1607,10 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder else statusDraft.text = new SpannableString(Html.fromHtml(statusDraft.content)).toString(); } + int max_car = MastodonHelper.getInstanceMaxChars(context); holder.binding.content.setText(statusDraft.text); + holder.binding.characterProgress.setMax(max_car); + updateCharacterCount(holder); holder.binding.content.setKeyBoardInputCallbackListener((inputContentInfo, flags, opts) -> { if (inputContentInfo != null) { Uri uri = inputContentInfo.getContentUri(); @@ -1694,7 +1722,6 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder if (instanceInfo == null) { return; } - int max_car = MastodonHelper.getInstanceMaxChars(context); holder.binding.characterProgress.setMax(max_car); holder.binding.contentSpoiler.addTextChangedListener(new TextWatcher() { @Override @@ -1917,27 +1944,13 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder 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; + case 300 -> composePollBinding.pollDuration.setSelection(0); + case 1800 -> composePollBinding.pollDuration.setSelection(1); + case 3600 -> composePollBinding.pollDuration.setSelection(2); + case 21600 -> composePollBinding.pollDuration.setSelection(3); + case 86400 -> composePollBinding.pollDuration.setSelection(4); + case 259200 -> composePollBinding.pollDuration.setSelection(5); + case 604800 -> composePollBinding.pollDuration.setSelection(6); } if (statusDraft.poll.multiple) composePollBinding.pollType.check(R.id.poll_type_multiple); @@ -1980,40 +1993,24 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder int poll_duration_pos = composePollBinding.pollDuration.getSelectedItemPosition(); int selected_poll_type_id = composePollBinding.pollType.getCheckedButtonId(); - String choice1 = composePollBinding.option1.text.getText().toString().trim(); - String choice2 = composePollBinding.option2.text.getText().toString().trim(); + String choice1 = Objects.requireNonNull(composePollBinding.option1.text.getText()).toString().trim(); + String choice2 = Objects.requireNonNull(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 = selected_poll_type_id == R.id.poll_type_multiple; - 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; - } + int expire = switch (poll_duration_pos) { + case 0 -> 300; + case 1 -> 1800; + case 2 -> 3600; + case 3 -> 21600; + case 4 -> 86400; + case 5 -> 259200; + case 6 -> 604800; + default -> 864000; + }; statusDraft.poll.expire_in = expire; if (promptDraftListener != null) { promptDraftListener.promptDraft(); @@ -2094,7 +2091,7 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder } public interface ManageDrafts { - void onItemDraftAdded(int position); + void onItemDraftAdded(int position, String content); void onItemDraftDeleted(Status status, int position); |