diff --git a/mastodon/build.gradle b/mastodon/build.gradle index 5c48023c7..828e56947 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -70,6 +70,7 @@ dependencies { implementation 'com.squareup:otto:1.3.8' implementation 'de.psdev:async-otto:1.0.3' implementation 'org.parceler:parceler-api:1.1.12' + implementation 'com.github.bottom-software-foundation:bottom-java:2.1.0' annotationProcessor 'org.parceler:parceler:1.1.12' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java index b857c9fbf..d0b5961e0 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java +++ b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java @@ -39,6 +39,7 @@ public class GlobalUserPreferences{ public static boolean showAltIndicator; public static boolean showNoAltIndicator; public static boolean enablePreReleases; + public static boolean bottomEncoding; public static String publishButtonText; public static ThemePreference theme; public static ColorPreference color; @@ -83,6 +84,7 @@ public class GlobalUserPreferences{ showAltIndicator=prefs.getBoolean("showAltIndicator", true); showNoAltIndicator=prefs.getBoolean("showNoAltIndicator", true); enablePreReleases=prefs.getBoolean("enablePreReleases", false); + bottomEncoding=prefs.getBoolean("bottomEncoding", false); publishButtonText=prefs.getString("publishButtonText", ""); theme=ThemePreference.values()[prefs.getInt("theme", 0)]; recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new HashMap<>()); @@ -121,6 +123,7 @@ public class GlobalUserPreferences{ .putBoolean("showNoAltIndicator", showNoAltIndicator) .putBoolean("enablePreReleases", enablePreReleases) .putString("publishButtonText", publishButtonText) + .putBoolean("bottomEncoding", bottomEncoding) .putInt("theme", theme.ordinal()) .putString("color", color.name()) .putString("recentLanguages", gson.toJson(recentLanguages)) diff --git a/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java b/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java index 99ea1eb4a..ddfb01371 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java +++ b/mastodon/src/main/java/org/joinmastodon/android/MainActivity.java @@ -154,6 +154,7 @@ public class MainActivity extends FragmentStackActivity{ ); Bundle currentArgs = currentFragment.getArguments(); if (this.fragmentContainers.size() == 1 + && currentArgs != null && currentArgs.getBoolean("_can_go_back", false) && currentArgs.containsKey("account")) { Bundle args = new Bundle(); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java index d12613176..07080f5f4 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java @@ -42,6 +42,7 @@ import android.text.TextWatcher; import android.text.format.DateFormat; import android.util.Log; import android.view.Gravity; +import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -66,6 +67,7 @@ import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; +import com.github.bottomSoftwareFoundation.bottom.Bottom; import com.twitter.twittertext.TwitterTextEmojiRegex; import org.joinmastodon.android.E; @@ -115,6 +117,7 @@ import org.joinmastodon.android.ui.views.LinkedTextView; import org.joinmastodon.android.ui.views.ReorderableLinearLayout; import org.joinmastodon.android.ui.views.SizeListenerLinearLayout; import org.joinmastodon.android.utils.MastodonLanguage; +import org.joinmastodon.android.utils.StringEncoder; import org.parceler.Parcel; import org.parceler.Parcels; @@ -155,11 +158,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private static final Pattern GLITCH_LOCAL_ONLY_PATTERN = Pattern.compile("[\\s\\S]*" + GLITCH_LOCAL_ONLY_SUFFIX + "[\uFE00-\uFE0F]*"); private static final String TAG="ComposeFragment"; - private static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE); + public static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE); // from https://github.com/mastodon/mastodon-ios/blob/main/Mastodon/Helper/MastodonRegex.swift - private static final Pattern AUTO_COMPLETE_PATTERN=Pattern.compile("(?{ + btn.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + if (!GlobalUserPreferences.bottomEncoding) { + GlobalUserPreferences.bottomEncoding = true; + GlobalUserPreferences.save(); + addBottomLanguage(allLanguagesMenu); + } + return false; + }); + languagePopup.setOnMenuItemClickListener(i->{ if (i.hasSubMenu()) return false; - updateLanguage(allLanguages.get(i.getItemId())); + if (i.getItemId() == allLanguages.size()) { + updateLanguage(language, "\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48", "bottom"); + encoding = "bottom"; + } else { + updateLanguage(allLanguages.get(i.getItemId())); + encoding = null; + } return true; }); } + private void addBottomLanguage(Menu menu) { + menu.add(0, allLanguages.size(), Menu.NONE, "bottom (\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48)"); + } + @Override public boolean onOptionsItemSelected(MenuItem item){ return true; @@ -995,6 +1028,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private void publish(boolean force){ String text=mainEditText.getText().toString(); CreateStatus.Request req=new CreateStatus.Request(); + if ("bottom".equals(encoding)) { + text = new StringEncoder(Bottom::encode).encode(text); + req.spoilerText = "bottom-encoded emoji spam"; + } if (localOnly && GlobalUserPreferences.accountsInGlitchMode.contains(accountID) && !GLITCH_LOCAL_ONLY_PATTERN.matcher(text).matches()) { @@ -1135,7 +1172,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr if (replyTo == null) { List newRecentLanguages = new ArrayList<>(Optional.ofNullable(recentLanguages.get(accountID)).orElse(defaultRecentLanguages)); newRecentLanguages.remove(language); + newRecentLanguages.remove(encoding); newRecentLanguages.add(0, language); + newRecentLanguages.add(0, encoding); recentLanguages.put(accountID, newRecentLanguages.stream().limit(4).collect(Collectors.toList())); GlobalUserPreferences.save(); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java index 669f3d36d..ebabad810 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java @@ -375,7 +375,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab } private void updateList(List addItems, Map items) { - if (addItems.size() == 0) return; + if (addItems.size() == 0 || getActivity() == null) return; for (int i = 0; i < addItems.size(); i++) items.put(View.generateViewId(), addItems.get(i)); updateOverflowMenu(); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java index 66259738e..a40797bdf 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java @@ -3,14 +3,16 @@ package org.joinmastodon.android.ui.displayitems; import android.app.Activity; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; import android.text.TextUtils; -import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.Button; import android.widget.TextView; +import android.widget.Toast; + +import com.github.bottomSoftwareFoundation.bottom.Bottom; +import com.github.bottomSoftwareFoundation.bottom.TranslationError; import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.R; @@ -20,13 +22,15 @@ import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Status; -import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable; import org.joinmastodon.android.model.StatusPrivacy; import org.joinmastodon.android.model.TranslatedStatus; import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.utils.CustomEmojiHelper; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.views.LinkedTextView; +import org.joinmastodon.android.utils.StringEncoder; + +import java.util.regex.Pattern; import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; @@ -46,6 +50,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ public boolean translated = false; public TranslatedStatus translation = null; private AccountSession session; + public static final Pattern BOTTOM_TEXT_PATTERN = Pattern.compile("(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️)(?:\uD83D\uDC49\uD83D\uDC48(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️))*\uD83D\uDC49\uD83D\uDC48"); public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status, boolean disableTranslate){ super(parentID, parentFragment); @@ -145,17 +150,31 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null && instanceInfo.v2.configuration.translation.enabled; - translateWrap.setVisibility( - (!GlobalUserPreferences.translateButtonOpenedOnly || item.textSelectable) && + boolean isBottomText = BOTTOM_TEXT_PATTERN.matcher(item.status.getStrippedText()).find(); + translateWrap.setVisibility((isBottomText || ( translateEnabled && !item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) && item.status.language != null && - (item.session.preferences == null || !item.status.language.equalsIgnoreCase(item.session.preferences.postingDefaultLanguage)) - ? View.VISIBLE : View.GONE); + (item.session.preferences == null || !item.status.language.equalsIgnoreCase(item.session.preferences.postingDefaultLanguage)))) + && (!GlobalUserPreferences.translateButtonOpenedOnly || item.textSelectable) + ? View.VISIBLE : View.GONE + ); translateButton.setText(item.translated ? R.string.sk_translate_show_original : R.string.sk_translate_post); - translateInfo.setText(item.translated ? itemView.getResources().getString(R.string.sk_translated_using, item.translation.provider) : ""); + translateInfo.setText(item.translated ? itemView.getResources().getString(R.string.sk_translated_using, isBottomText ? "bottom-java" : item.translation.provider) : ""); translateButton.setOnClickListener(v->{ if (item.translation == null) { + if (isBottomText) { + try { + item.translation = new TranslatedStatus(); + item.translation.content = new StringEncoder(Bottom::decode).decode(item.status.getStrippedText(), BOTTOM_TEXT_PATTERN); + item.translated = true; + } catch (TranslationError err) { + item.translation = null; + Toast.makeText(itemView.getContext(), err.getLocalizedMessage(), Toast.LENGTH_SHORT).show(); + } + rebind(); + return; + } translateProgress.setVisibility(View.VISIBLE); translateButton.setClickable(false); translateButton.animate().alpha(0.5f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start(); diff --git a/mastodon/src/main/java/org/joinmastodon/android/utils/MastodonLanguage.java b/mastodon/src/main/java/org/joinmastodon/android/utils/MastodonLanguage.java index 2e4eb949a..a184032d9 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/utils/MastodonLanguage.java +++ b/mastodon/src/main/java/org/joinmastodon/android/utils/MastodonLanguage.java @@ -7,6 +7,7 @@ import android.content.res.Resources; import android.os.Build; import android.os.LocaleList; +import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.model.Instance; import java.util.ArrayList; diff --git a/mastodon/src/main/java/org/joinmastodon/android/utils/StringEncoder.java b/mastodon/src/main/java/org/joinmastodon/android/utils/StringEncoder.java new file mode 100644 index 000000000..ae18fe609 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/utils/StringEncoder.java @@ -0,0 +1,53 @@ +package org.joinmastodon.android.utils; + +import org.joinmastodon.android.fragments.ComposeFragment; + +import java.util.function.Function; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +// not a good class +public class StringEncoder { + private final Function fn; + + public StringEncoder(Function fn) { + this.fn = fn; + } + + // prettiest method award winner 2023 [citation needed] + public String encode(String content) { + StringBuilder encodedString = new StringBuilder(); + // matches mentions and hashtags + Matcher m = ComposeFragment.HIGHLIGHT_PATTERN.matcher(content); + int previousEnd = 0; + while (m.find()) { + MatchResult res = m.toMatchResult(); + // everything before the match - do encode + encodedString.append(fn.apply(content.substring(previousEnd, res.start()))); + previousEnd = res.end(); + // the match - do not encode + encodedString.append(res.group()); + } + // everything after the last match - do encode + encodedString.append(fn.apply(content.substring(previousEnd))); + return encodedString.toString(); + } + + // prettiest almost-exact replica of a pretty function + public String decode(String content, Pattern regex) { + Matcher m = regex.matcher(content); + StringBuilder decodedString = new StringBuilder(); + int previousEnd = 0; + while (m.find()) { + MatchResult res = m.toMatchResult(); + // everything before the match - do not decode + decodedString.append(content.substring(previousEnd, res.start())); + previousEnd = res.end(); + // the match - do decode + decodedString.append(fn.apply(res.group())); + } + decodedString.append(content.substring(previousEnd)); + return decodedString.toString(); + } +} diff --git a/settings.gradle b/settings.gradle index fce12fb78..36baa893b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,6 +4,7 @@ dependencyResolutionManagement { google() mavenCentral() mavenLocal() + maven { url 'https://jitpack.io' } } } rootProject.name = "Megalodon"