From 1b4dc01c74e3db80051da24aa0e4ebc4f8cd571d Mon Sep 17 00:00:00 2001 From: Grishka Date: Tue, 12 Sep 2023 06:00:40 +0300 Subject: [PATCH] Post translation closes #267, closes #671, closes #502 --- .../requests/statuses/TranslateStatus.java | 13 +++ .../fragments/BaseStatusListFragment.java | 55 +++++++++++++ .../joinmastodon/android/model/Status.java | 18 +++++ .../android/model/Translation.java | 10 +++ .../displayitems/HeaderStatusDisplayItem.java | 12 +++ .../displayitems/TextStatusDisplayItem.java | 79 ++++++++++++++++++- .../src/main/res/layout/display_item_text.xml | 13 ++- .../res/layout/footer_text_translation.xml | 44 +++++++++++ mastodon/src/main/res/menu/post.xml | 2 +- mastodon/src/main/res/values/strings.xml | 7 ++ 10 files changed, 246 insertions(+), 7 deletions(-) create mode 100644 mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/TranslateStatus.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/model/Translation.java create mode 100644 mastodon/src/main/res/layout/footer_text_translation.xml diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/TranslateStatus.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/TranslateStatus.java new file mode 100644 index 000000000..881036272 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/TranslateStatus.java @@ -0,0 +1,13 @@ +package org.joinmastodon.android.api.requests.statuses; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.Translation; + +import java.util.Map; + +public class TranslateStatus extends MastodonAPIRequest{ + public TranslateStatus(String id, String lang){ + super(HttpMethod.POST, "/statuses/"+id+"/translate", Translation.class); + setRequestBody(Map.of("lang", lang)); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java index a9152c67b..c141029a5 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -17,13 +17,16 @@ import org.joinmastodon.android.E; import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.polls.SubmitPollVote; +import org.joinmastodon.android.api.requests.statuses.TranslateStatus; import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.DisplayItemsParent; import org.joinmastodon.android.model.Poll; import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.model.Translation; import org.joinmastodon.android.ui.BetterItemAnimator; +import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem; @@ -33,6 +36,7 @@ import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; +import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem; import org.joinmastodon.android.ui.photoviewer.PhotoViewer; import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost; import org.joinmastodon.android.ui.utils.MediaAttachmentViewController; @@ -43,6 +47,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -560,6 +565,56 @@ public abstract class BaseStatusListFragment exten return attachmentViewsPool; } + public void togglePostTranslation(Status status, String itemID){ + switch(status.translationState){ + case LOADING -> { + return; + } + case SHOWN -> { + status.translationState=Status.TranslationState.HIDDEN; + } + case HIDDEN -> { + if(status.translation!=null){ + status.translationState=Status.TranslationState.SHOWN; + }else{ + status.translationState=Status.TranslationState.LOADING; + new TranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage()) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(Translation result){ + if(getActivity()==null) + return; + status.translation=result; + status.translationState=Status.TranslationState.SHOWN; + TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class); + if(text!=null){ + text.updateTranslation(true); + imgLoader.bindViewHolder((ImageLoaderRecyclerAdapter) list.getAdapter(), text, text.getAbsoluteAdapterPosition()); + } + } + + @Override + public void onError(ErrorResponse error){ + if(getActivity()==null) + return; + new M3AlertDialogBuilder(getActivity()) + .setTitle(R.string.error) + .setMessage(R.string.translation_failed) + .setPositiveButton(R.string.ok, null) + .show(); + } + }) + .exec(accountID); + } + } + } + TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class); + if(text!=null){ + text.updateTranslation(true); + imgLoader.bindViewHolder((ImageLoaderRecyclerAdapter) list.getAdapter(), text, text.getAbsoluteAdapterPosition()); + } + } + public void rebuildAllDisplayItems(){ displayItems.clear(); for(T item:data){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Status.java b/mastodon/src/main/java/org/joinmastodon/android/model/Status.java index fcfcb8c1d..caeba6bb8 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Status.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Status.java @@ -1,5 +1,7 @@ package org.joinmastodon.android.model; +import android.text.TextUtils; + import org.joinmastodon.android.api.ObjectValidationException; import org.joinmastodon.android.api.RequiredField; import org.joinmastodon.android.events.StatusCountersUpdatedEvent; @@ -8,6 +10,8 @@ import org.parceler.Parcel; import java.time.Instant; import java.util.List; +import java.util.Locale; +import java.util.Objects; import androidx.annotation.NonNull; @@ -61,6 +65,8 @@ public class Status extends BaseModel implements DisplayItemsParent{ public transient boolean spoilerRevealed; public transient boolean hasGapAfter; private transient String strippedText; + public transient TranslationState translationState=TranslationState.HIDDEN; + public transient Translation translation; public Status(){} @@ -161,6 +167,18 @@ public class Status extends BaseModel implements DisplayItemsParent{ public Status clone(){ Status copy=(Status) super.clone(); copy.spoilerRevealed=false; + copy.translationState=TranslationState.HIDDEN; return copy; } + + public boolean isEligibleForTranslation(){ + return !TextUtils.isEmpty(content) && !TextUtils.isEmpty(language) && !Objects.equals(Locale.getDefault().getLanguage(), language) + && (visibility==StatusPrivacy.PUBLIC || visibility==StatusPrivacy.UNLISTED); + } + + public enum TranslationState{ + HIDDEN, + SHOWN, + LOADING + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Translation.java b/mastodon/src/main/java/org/joinmastodon/android/model/Translation.java new file mode 100644 index 000000000..68487451d --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Translation.java @@ -0,0 +1,10 @@ +package org.joinmastodon.android.model; + +import org.joinmastodon.android.api.AllFieldsAreRequired; + +@AllFieldsAreRequired +public class Translation extends BaseModel{ + public String content; + public String detectedSourceLanguage; + public String provider; +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java index f8055c226..b355b04f8 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java @@ -41,6 +41,7 @@ import org.parceler.Parcels; import java.time.Instant; import java.util.Collections; import java.util.List; +import java.util.Locale; import androidx.annotation.LayoutRes; import me.grishka.appkit.Nav; @@ -195,6 +196,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked); }else if(id==R.id.share){ UiUtils.openSystemShareSheet(activity, item.status.url); + }else if(id==R.id.translate){ + item.parentFragment.togglePostTranslation(item.status, item.parentID); } return true; }); @@ -285,6 +288,15 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ Account account=item.user; Menu menu=optionsMenu.getMenu(); boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account); + boolean canTranslate=item.status!=null && item.status.getContentStatus().isEligibleForTranslation(); + MenuItem translate=menu.findItem(R.id.translate); + translate.setVisible(canTranslate); + if(canTranslate){ + if(item.status.translationState==Status.TranslationState.SHOWN) + translate.setTitle(R.string.translation_show_original); + else + translate.setTitle(item.parentFragment.getString(R.string.translate_post, Locale.forLanguageTag(item.status.getContentStatus().language).getDisplayLanguage())); + } menu.findItem(R.id.edit).setVisible(item.status!=null && isOwnPost); menu.findItem(R.id.delete).setVisible(item.status!=null && isOwnPost); menu.findItem(R.id.open_in_browser).setVisible(item.status!=null); 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 6a413bbc1..d9a0a7e00 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,15 +3,24 @@ package org.joinmastodon.android.ui.displayitems; import android.app.Activity; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; +import android.text.SpannableStringBuilder; +import android.view.View; import android.view.ViewGroup; +import android.view.ViewStub; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; import org.joinmastodon.android.R; import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.model.Status; +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 java.util.Locale; + import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.MovieDrawable; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; @@ -20,6 +29,8 @@ import me.grishka.appkit.utils.V; public class TextStatusDisplayItem extends StatusDisplayItem{ private CharSequence text; private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(); + private CharSequence translatedText; + private CustomEmojiHelper translationEmojiHelper=new CustomEmojiHelper(); public boolean textSelectable; public boolean reduceTopPadding; public final Status status; @@ -38,30 +49,54 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ @Override public int getImageCount(){ - return emojiHelper.getImageCount(); + return getCurrentEmojiHelper().getImageCount(); } @Override public ImageLoaderRequest getImageRequest(int index){ - return emojiHelper.getImageRequest(index); + return getCurrentEmojiHelper().getImageRequest(index); + } + + public void setTranslatedText(String text){ + Status statusForContent=status.getContentStatus(); + translatedText=HtmlParser.parse(text, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, parentFragment.getAccountID()); + translationEmojiHelper.setText(translatedText); + } + + private CustomEmojiHelper getCurrentEmojiHelper(){ + return status.translationState==Status.TranslationState.SHOWN ? translationEmojiHelper : emojiHelper; } public static class Holder extends StatusDisplayItem.Holder implements ImageLoaderViewHolder{ private final LinkedTextView text; + private final ViewStub translationFooterStub; + private View translationFooter; + private TextView translationInfo; + private Button translationShowOriginal; + private ProgressBar translationProgress; public Holder(Activity activity, ViewGroup parent){ super(activity, R.layout.display_item_text, parent); text=findViewById(R.id.text); + translationFooterStub=findViewById(R.id.translation_info); } @Override public void onBind(TextStatusDisplayItem item){ - text.setText(item.text); + if(item.status.translationState==Status.TranslationState.SHOWN){ + if(item.translatedText==null){ + item.setTranslatedText(item.status.translation.content); + } + text.setText(item.translatedText); + }else{ + text.setText(item.text); + } text.setTextIsSelectable(item.textSelectable); text.setInvalidateOnEveryFrame(false); itemView.setClickable(false); text.setPadding(text.getPaddingLeft(), item.reduceTopPadding ? V.dp(8) : V.dp(16), text.getPaddingRight(), text.getPaddingBottom()); text.setTextColor(UiUtils.getThemeColor(text.getContext(), item.inset ? R.attr.colorM3OnSurfaceVariant : R.attr.colorM3OnSurface)); + updateTranslation(false); } @Override @@ -84,5 +119,43 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ private CustomEmojiHelper getEmojiHelper(){ return item.emojiHelper; } + + public void updateTranslation(boolean updateText){ + if(item.status==null) + return; + if(item.status.translationState==Status.TranslationState.HIDDEN){ + if(translationFooter!=null) + translationFooter.setVisibility(View.GONE); + if(updateText){ + text.setText(item.text); + } + }else{ + if(translationFooter==null){ + translationFooter=translationFooterStub.inflate(); + translationInfo=findViewById(R.id.translation_info_text); + translationShowOriginal=findViewById(R.id.translation_show_original); + translationProgress=findViewById(R.id.translation_progress); + translationShowOriginal.setOnClickListener(v->item.parentFragment.togglePostTranslation(item.status, item.parentID)); + }else{ + translationFooter.setVisibility(View.VISIBLE); + } + if(item.status.translationState==Status.TranslationState.SHOWN){ + translationProgress.setVisibility(View.GONE); + translationInfo.setVisibility(View.VISIBLE); + translationShowOriginal.setVisibility(View.VISIBLE); + translationInfo.setText(translationInfo.getContext().getString(R.string.post_translated, Locale.forLanguageTag(item.status.translation.detectedSourceLanguage).getDisplayLanguage(), item.status.translation.provider)); + if(updateText){ + if(item.translatedText==null){ + item.setTranslatedText(item.status.translation.content); + } + text.setText(item.translatedText); + } + }else{ // LOADING + translationProgress.setVisibility(View.VISIBLE); + translationInfo.setVisibility(View.INVISIBLE); + translationShowOriginal.setVisibility(View.INVISIBLE); + } + } + } } } diff --git a/mastodon/src/main/res/layout/display_item_text.xml b/mastodon/src/main/res/layout/display_item_text.xml index 4620043f1..db9ece18e 100644 --- a/mastodon/src/main/res/layout/display_item_text.xml +++ b/mastodon/src/main/res/layout/display_item_text.xml @@ -1,8 +1,9 @@ - + android:layout_height="wrap_content" + android:orientation="vertical"> - \ No newline at end of file + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/footer_text_translation.xml b/mastodon/src/main/res/layout/footer_text_translation.xml new file mode 100644 index 000000000..b80e730f8 --- /dev/null +++ b/mastodon/src/main/res/layout/footer_text_translation.xml @@ -0,0 +1,44 @@ + + + + + + + + + +