diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/instance/GetInstance.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/instance/GetInstance.java index 84273ce22..2ed250b96 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/instance/GetInstance.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/instance/GetInstance.java @@ -7,4 +7,15 @@ public class GetInstance extends MastodonAPIRequest{ public GetInstance(){ super(HttpMethod.GET, "/instance", Instance.class); } + + public static class V2 extends MastodonAPIRequest{ + public V2(){ + super(HttpMethod.GET, "/instance", Instance.V2.class); + } + + @Override + protected String getPathPrefix() { + return "/api/v2"; + } + } } 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..bb3c29888 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/TranslateStatus.java @@ -0,0 +1,11 @@ +package org.joinmastodon.android.api.requests.statuses; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.TranslatedStatus; + +public class TranslateStatus extends MastodonAPIRequest { + public TranslateStatus(String id) { + super(HttpMethod.POST, "/statuses/"+id+"/translate", TranslatedStatus.class); + setRequestBody(new Object()); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java index 2bd331573..f1de0f707 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java @@ -7,6 +7,7 @@ import org.joinmastodon.android.api.StatusInteractionController; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Application; import org.joinmastodon.android.model.Filter; +import org.joinmastodon.android.model.Preferences; import org.joinmastodon.android.model.PushSubscription; import org.joinmastodon.android.model.Token; @@ -28,6 +29,7 @@ public class AccountSession{ public long filtersLastUpdated; public List wordFilters=new ArrayList<>(); public String pushAccountID; + public Preferences preferences; private transient MastodonAPIController apiController; private transient StatusInteractionController statusInteractionController; private transient CacheController cacheController; diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java index 52c27480f..20580ea60 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java @@ -22,6 +22,7 @@ import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.R; import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.PushSubscriptionManager; +import org.joinmastodon.android.api.requests.accounts.GetPreferences; import org.joinmastodon.android.api.requests.accounts.GetWordFilters; import org.joinmastodon.android.api.requests.instance.GetCustomEmojis; import org.joinmastodon.android.api.requests.accounts.GetOwnAccount; @@ -34,6 +35,7 @@ import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.EmojiCategory; import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Instance; +import org.joinmastodon.android.model.Preferences; import org.joinmastodon.android.model.Token; import java.io.File; @@ -248,12 +250,13 @@ public class AccountSessionManager{ HashSet domains=new HashSet<>(); for(AccountSession session:sessions.values()){ domains.add(session.domain.toLowerCase()); - if(now-session.infoLastUpdated>24L*3600_000L){ - updateSessionLocalInfo(session); - } - if(now-session.filtersLastUpdated>3600_000L){ - updateSessionWordFilters(session); - } +// if(now-session.infoLastUpdated>24L*3600_000L){ + updateSessionPreferences(session); + updateSessionLocalInfo(session); +// } +// if(now-session.filtersLastUpdated>3600_000L){ + updateSessionWordFilters(session); +// } } if(loadedInstances){ maybeUpdateCustomEmojis(domains); @@ -263,10 +266,10 @@ public class AccountSessionManager{ private void maybeUpdateCustomEmojis(Set domains){ long now=System.currentTimeMillis(); for(String domain:domains){ - Long lastUpdated=instancesLastUpdated.get(domain); - if(lastUpdated==null || now-lastUpdated>24L*3600_000L){ - updateInstanceInfo(domain); - } +// Long lastUpdated=instancesLastUpdated.get(domain); +// if(lastUpdated==null || now-lastUpdated>24L*3600_000L){ + updateInstanceInfo(domain); +// } } } @@ -288,6 +291,18 @@ public class AccountSessionManager{ .exec(session.getID()); } + private void updateSessionPreferences(AccountSession session){ + new GetPreferences().setCallback(new Callback<>() { + @Override + public void onSuccess(Preferences preferences) { + session.preferences=preferences; + } + + @Override + public void onError(ErrorResponse error) {} + }).exec(session.getID()); + } + private void updateSessionWordFilters(AccountSession session){ new GetWordFilters() .setCallback(new Callback<>(){ @@ -313,6 +328,11 @@ public class AccountSessionManager{ public void onSuccess(Instance instance){ instances.put(domain, instance); updateInstanceEmojis(instance, domain); + try { + if (Integer.parseInt(instance.version.split("\\.")[0]) >= 4) { + updateInstanceInfoV2(domain); + } + } catch (Exception ignored) {} } @Override @@ -323,6 +343,19 @@ public class AccountSessionManager{ .execNoAuth(domain); } + public void updateInstanceInfoV2(String domain) { + new GetInstance.V2().setCallback(new Callback<>() { + @Override + public void onSuccess(Instance.V2 v2) { + Instance instanceInfo = instances.get(domain); + if (instanceInfo != null) instanceInfo.v2 = v2; + } + + @Override + public void onError(ErrorResponse errorResponse) {} + }).execNoAuth(domain); + } + private void updateInstanceEmojis(Instance instance, String domain){ new GetCustomEmojis() .setCallback(new Callback<>(){ @@ -398,6 +431,10 @@ public class AccountSessionManager{ return instances.get(domain); } + public Instance getInstanceInfoForAccount(String account) { + return AccountSessionManager.getInstance().getInstanceInfo(instance.getAccount(account).domain); + } + public void updateAccountInfo(String id, Account account){ AccountSession session=getAccount(id); session.self=account; diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java b/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java index a9a0aeab7..d2bba026f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java @@ -82,6 +82,8 @@ public class Instance extends BaseModel{ // non-standard field in some Mastodon forks public int maxTootChars; + public V2 v2; + @Override public void postprocess() throws ObjectValidationException{ super.postprocess(); @@ -176,4 +178,19 @@ public class Instance extends BaseModel{ public int minExpiration; public int maxExpiration; } + + @Parcel + public static class V2 extends BaseModel { + public V2.Configuration configuration; + + @Parcel + public static class Configuration { + public TranslationConfiguration translation; + } + + @Parcel + public static class TranslationConfiguration{ + public boolean enabled; + } + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/TranslatedStatus.java b/mastodon/src/main/java/org/joinmastodon/android/model/TranslatedStatus.java new file mode 100644 index 000000000..891c86c03 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/model/TranslatedStatus.java @@ -0,0 +1,7 @@ +package org.joinmastodon.android.model; + +public class TranslatedStatus extends BaseModel { + public String content; + public String detectedSourceLanguage; + public String provider; +} 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 557bede7e..06bbb1f70 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 @@ -6,15 +6,24 @@ import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; import android.widget.TextView; import org.joinmastodon.android.R; +import org.joinmastodon.android.api.requests.statuses.TranslateStatus; +import org.joinmastodon.android.api.session.AccountSession; +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.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.views.LinkedTextView; +import me.grishka.appkit.api.Callback; +import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.MovieDrawable; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; @@ -25,6 +34,12 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ private CharSequence parsedSpoilerText; public boolean textSelectable; public final Status status; + public boolean translated = false; + public TranslatedStatus translation = null; + + private AccountSession session; + private Instance instanceInfo; + private boolean translateEnabled; public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status){ super(parentID, parentFragment); @@ -36,6 +51,9 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ spoilerEmojiHelper=new CustomEmojiHelper(); spoilerEmojiHelper.setText(parsedSpoilerText); } + session = AccountSessionManager.getInstance().getAccount(parentFragment.getAccountID()); + instanceInfo = AccountSessionManager.getInstance().getInstanceInfo(session.domain); + translateEnabled = instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null && instanceInfo.v2.configuration.translation.enabled; } @Override @@ -59,38 +77,73 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ public static class Holder extends StatusDisplayItem.Holder implements ImageLoaderViewHolder{ private final LinkedTextView text; - private final TextView spoilerTitle; - private final View spoilerOverlay; + private final TextView spoilerTitle, translateInfo; + private final View spoilerOverlay, textWrap, translateWrap; + private final Button translateButton; public Holder(Activity activity, ViewGroup parent){ super(activity, R.layout.display_item_text, parent); text=findViewById(R.id.text); spoilerTitle=findViewById(R.id.spoiler_title); spoilerOverlay=findViewById(R.id.spoiler_overlay); + textWrap=findViewById(R.id.text_wrap); + translateWrap=findViewById(R.id.translate_wrap); + translateButton=findViewById(R.id.translate_btn); + translateInfo=findViewById(R.id.translate_info); itemView.setOnClickListener(v->item.parentFragment.onRevealSpoilerClick(this)); } @Override public void onBind(TextStatusDisplayItem item){ - text.setText(item.text); + text.setText(item.translated + ? HtmlParser.parse(item.translation.content, item.status.emojis, item.status.mentions, item.status.tags, item.parentFragment.getAccountID()) + : item.text); text.setTextIsSelectable(item.textSelectable); text.setInvalidateOnEveryFrame(false); + if(!TextUtils.isEmpty(item.status.spoilerText)){ spoilerTitle.setText(item.parsedSpoilerText); if(item.status.spoilerRevealed){ spoilerOverlay.setVisibility(View.GONE); - text.setVisibility(View.VISIBLE); + textWrap.setVisibility(View.VISIBLE); itemView.setClickable(false); }else{ spoilerOverlay.setVisibility(View.VISIBLE); - text.setVisibility(View.INVISIBLE); + textWrap.setVisibility(View.INVISIBLE); itemView.setClickable(true); } }else{ spoilerOverlay.setVisibility(View.GONE); - text.setVisibility(View.VISIBLE); + textWrap.setVisibility(View.VISIBLE); itemView.setClickable(false); } + + translateWrap.setVisibility(item.textSelectable && item.translateEnabled && + !item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) && + (item.session.preferences == null || !item.status.language.equalsIgnoreCase(item.session.preferences.postingDefaultLanguage)) + ? View.VISIBLE : View.GONE); + translateButton.setText(item.translated ? R.string.translate_show_original : R.string.translate_post); + translateInfo.setText(item.translated ? itemView.getResources().getString(R.string.translated_using, item.translation.provider) : ""); + translateButton.setOnClickListener(v->{ + if (item.translation == null) { + new TranslateStatus(item.status.id).setCallback(new Callback<>() { + @Override + public void onSuccess(TranslatedStatus translatedStatus) { + item.translation = translatedStatus; + item.translated = true; + rebind(); + } + + @Override + public void onError(ErrorResponse error) { + error.showToast(itemView.getContext()); + } + }).exec(item.parentFragment.getAccountID()); + } else { + item.translated = !item.translated; + rebind(); + } + }); } @Override diff --git a/mastodon/src/main/res/layout/display_item_text.xml b/mastodon/src/main/res/layout/display_item_text.xml index 90d06c6a8..baf06cff7 100644 --- a/mastodon/src/main/res/layout/display_item_text.xml +++ b/mastodon/src/main/res/layout/display_item_text.xml @@ -3,23 +3,62 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingLeft="16dp" - android:paddingRight="16dp" android:paddingTop="10dp" android:paddingBottom="12dp"> - + android:orientation="vertical"> + + + + + +