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 index bb3c29888..881036272 100644 --- 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 @@ -1,11 +1,13 @@ package org.joinmastodon.android.api.requests.statuses; import org.joinmastodon.android.api.MastodonAPIRequest; -import org.joinmastodon.android.model.TranslatedStatus; +import org.joinmastodon.android.model.Translation; -public class TranslateStatus extends MastodonAPIRequest { - public TranslateStatus(String id) { - super(HttpMethod.POST, "/statuses/"+id+"/translate", TranslatedStatus.class); - setRequestBody(new Object()); - } +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/api/session/AccountSession.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java index 90b56f9b7..c4c7fe09e 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 @@ -303,6 +303,10 @@ public class AccountSession{ }); } + public void updateAccountInfo(){ + AccountSessionManager.getInstance().updateSessionLocalInfo(this); + } + public Optional getInstance() { return Optional.ofNullable(AccountSessionManager.getInstance().getInstanceInfo(domain)); } @@ -319,8 +323,4 @@ public class AccountSession{ .map(instance->"https://"+domain+(instance.isAkkoma() ? "/images/avi.png" : "/avatars/original/missing.png")) .orElse(""); } - - public void updateAccountInfo(){ - AccountSessionManager.getInstance().updateSessionLocalInfo(this); - } } 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 2682c92f8..ba90f5d80 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 @@ -314,8 +314,7 @@ public class AccountSessionManager{ } } - - public void updateSessionLocalInfo(AccountSession session){ + /*package*/ void updateSessionLocalInfo(AccountSession session){ new GetOwnAccount() .setCallback(new Callback<>(){ @Override 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 d3d16d4e8..55796ca85 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -26,6 +26,8 @@ import org.joinmastodon.android.GlobalUserPreferences; 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.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.events.PollUpdatedEvent; import org.joinmastodon.android.model.Account; @@ -862,6 +864,61 @@ public abstract class BaseStatusListFragment exten assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon())); } + 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; + status.translationState=Status.TranslationState.HIDDEN; + TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class); + if(text!=null){ + text.updateTranslation(true); + } + 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/fragments/ComposeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java index af183ee17..f947bbf37 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java @@ -32,6 +32,7 @@ import android.text.TextWatcher; import android.text.format.DateFormat; import android.text.style.BackgroundColorSpan; import android.text.style.ForegroundColorSpan; +import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; @@ -909,6 +910,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr languageButton = wrap.findViewById(R.id.language_btn); languageButton.setOnClickListener(v->showLanguageAlert()); + languageButton.setOnLongClickListener(v->{ + languageButton.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + if(!getLocalPrefs().bottomEncoding){ + getLocalPrefs().bottomEncoding=true; + getLocalPrefs().save(); + } + return false; + }); if(GlobalUserPreferences.relocatePublishButton){ publishButtonRelocated.setOnClickListener(v -> { diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/FeaturedHashtagsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/FeaturedHashtagsListFragment.java index c9234ea4c..2e506e2cd 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/FeaturedHashtagsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/FeaturedHashtagsListFragment.java @@ -45,8 +45,8 @@ public class FeaturedHashtagsListFragment extends BaseStatusListFragment Objects.equals(h.name, hashtag)).findAny().map(h -> h.following).orElse(null)); + public void onItemClick(String id){ + UiUtils.openHashtagTimeline(getActivity(), accountID, Objects.requireNonNull(findItemOfType(id, HashtagStatusDisplayItem.class)).tag); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowedHashtagsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowedHashtagsFragment.java index bdccae2b5..b6551f75a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowedHashtagsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowedHashtagsFragment.java @@ -122,7 +122,7 @@ public class FollowedHashtagsFragment extends MastodonRecyclerFragment @Override public void onClick() { - UiUtils.openHashtagTimeline(getActivity(), accountID, item.name, item.following); + UiUtils.openHashtagTimeline(getActivity(), accountID, item.name); } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java index 60a7ebf1c..39b8e16de 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java @@ -1,35 +1,33 @@ package org.joinmastodon.android.fragments; import android.app.Activity; +import android.content.res.TypedArray; import android.net.Uri; import android.os.Bundle; +import android.text.SpannableStringBuilder; import android.view.HapticFeedbackConstants; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ProgressBar; +import android.widget.TextView; import android.widget.Toast; -import org.joinmastodon.android.E; import org.joinmastodon.android.R; -import org.joinmastodon.android.api.requests.filters.CreateFilter; -import org.joinmastodon.android.api.requests.filters.DeleteFilter; -import org.joinmastodon.android.api.requests.filters.GetFilters; -import org.joinmastodon.android.api.requests.tags.GetHashtag; -import org.joinmastodon.android.api.requests.tags.SetHashtagFollowed; +import org.joinmastodon.android.api.MastodonErrorResponse; +import org.joinmastodon.android.api.requests.tags.GetTag; +import org.joinmastodon.android.api.requests.tags.SetTagFollowed; import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline; -import org.joinmastodon.android.events.HashtagUpdatedEvent; -import org.joinmastodon.android.fragments.settings.EditFilterFragment; -import org.joinmastodon.android.model.Filter; -import org.joinmastodon.android.model.FilterAction; import org.joinmastodon.android.model.FilterContext; -import org.joinmastodon.android.model.FilterKeyword; import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.TimelineDefinition; +import org.joinmastodon.android.ui.text.SpacerSpan; import org.joinmastodon.android.ui.utils.UiUtils; -import org.joinmastodon.android.utils.StatusFilterPredicate; +import org.joinmastodon.android.ui.views.ProgressBarButton; import org.parceler.Parcels; import java.util.ArrayList; @@ -42,118 +40,54 @@ import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.SimpleCallback; +import me.grishka.appkit.utils.MergeRecyclerAdapter; +import me.grishka.appkit.utils.SingleViewRecyclerAdapter; import me.grishka.appkit.utils.V; -public class HashtagTimelineFragment extends PinnableStatusListFragment { - private String hashtag; +public class HashtagTimelineFragment extends PinnableStatusListFragment{ + private Hashtag hashtag; + private String hashtagName; + private TextView headerTitle, headerSubtitle; + private ProgressBarButton followButton; + private ProgressBar followProgress; + private MenuItem followMenuItem, pinMenuItem, muteMenuItem; + private boolean followRequestRunning; + private boolean toolbarContentVisible; + private List any; private List all; private List none; private boolean following; private boolean localOnly; - private MenuItem followButton; - private MenuItem muteButton; - private Optional filter = Optional.empty(); + private Menu optionsMenu; + private MenuInflater optionsMenuInflater; @Override protected boolean wantsComposeButton() { return true; } - @Override public void onAttach(Activity activity){ super.onAttach(activity); - updateTitle(getArguments().getString("hashtag")); following=getArguments().getBoolean("following", false); localOnly=getArguments().getBoolean("localOnly", false); any=getArguments().getStringArrayList("any"); all=getArguments().getStringArrayList("all"); none=getArguments().getStringArrayList("none"); + if(getArguments().containsKey("hashtag")){ + hashtag=Parcels.unwrap(getArguments().getParcelable("hashtag")); + hashtagName=hashtag.name; + }else{ + hashtagName=getArguments().getString("hashtagName"); + } + setTitle('#'+hashtagName); setHasOptionsMenu(true); } - private void updateTitle(String hashtagName) { - hashtag = hashtagName; - setTitle('#'+hashtag); - } - - private void updateFollowingState(boolean newFollowing) { - this.following = newFollowing; - followButton.setTitle(getString(newFollowing ? R.string.unfollow_user : R.string.follow_user, "#" + hashtag)); - followButton.setIcon(newFollowing ? R.drawable.ic_fluent_person_delete_24_filled : R.drawable.ic_fluent_person_add_24_regular); - E.post(new HashtagUpdatedEvent(hashtag, following)); - } - private void updateMuteState(boolean newMute) { - muteButton.setTitle(getString(newMute ? R.string.unmute_user : R.string.mute_user, "#" + hashtag)); - muteButton.setIcon(newMute ? R.drawable.ic_fluent_speaker_2_24_regular : R.drawable.ic_fluent_speaker_off_24_regular); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.hashtag_timeline, menu); - super.onCreateOptionsMenu(menu, inflater); - followButton = menu.findItem(R.id.follow_hashtag); - updateFollowingState(following); - muteButton = menu.findItem(R.id.mute_hashtag); - updateMuteState(filter.isPresent()); - new GetHashtag(hashtag).setCallback(new Callback<>() { - @Override - public void onSuccess(Hashtag hashtag) { - if (getActivity() == null) return; - updateTitle(hashtag.name); - updateFollowingState(hashtag.following); - } - - @Override - public void onError(ErrorResponse error) { - error.showToast(getActivity()); - } - }).exec(accountID); - - new GetFilters().setCallback(new Callback<>() { - @Override - public void onSuccess(List filters) { - if (getActivity() == null) return; - filter=filters.stream().filter(filter->filter.title.equals("#"+hashtag)).findAny(); - updateMuteState(filter.isPresent()); - } - - @Override - public void onError(ErrorResponse error) { - error.showToast(getActivity()); - } - }).exec(accountID); - } - - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (super.onOptionsItemSelected(item)) return true; - if (item.getItemId() == R.id.follow_hashtag) { - updateFollowingState(!following); - getToolbar().performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK); - new SetHashtagFollowed(hashtag, following).setCallback(new Callback<>() { - @Override - public void onSuccess(Hashtag i) { - if (getActivity() == null) return; - if (i.following == following) Toast.makeText(getActivity(), getString(i.following ? R.string.followed_user : R.string.unfollowed_user, "#" + i.name), Toast.LENGTH_SHORT).show(); - updateFollowingState(i.following); - } - - @Override - public void onError(ErrorResponse error) { - error.showToast(getActivity()); - updateFollowingState(!following); - } - }).exec(accountID); - return true; - } else if (item.getItemId() == R.id.mute_hashtag) { - showMuteDialog(filter.isPresent()); - return true; - } - return false; + muteMenuItem.setTitle(getString(newMute ? R.string.unmute_user : R.string.mute_user, "#" + hashtag)); + muteMenuItem.setIcon(newMute ? R.drawable.ic_fluent_speaker_2_24_regular : R.drawable.ic_fluent_speaker_off_24_regular); } private void showMuteDialog(boolean mute) { @@ -208,12 +142,10 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment { @Override protected void doLoadData(int offset, int count){ - currentRequest=new GetHashtagTimeline(hashtag, offset==0 ? null : getMaxID(), null, count, any, all, none, localOnly, getLocalPrefs().timelineReplyVisibility) + currentRequest=new GetHashtagTimeline(hashtagName, offset==0 ? null : getMaxID(), null, count, any, all, none, localOnly, getLocalPrefs().timelineReplyVisibility) .setCallback(new SimpleCallback<>(this){ @Override public void onSuccess(List result){ - if (getActivity() == null) return; - result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList()); onDataLoaded(result, !result.isEmpty()); } }) @@ -227,6 +159,36 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment { loadData(); } + @Override + public void loadData(){ + reloadTag(); + super.loadData(); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState){ + super.onViewCreated(view, savedInstanceState); + fab=view.findViewById(R.id.fab); + fab.setOnClickListener(this::onFabClick); + + if(getParentFragment() instanceof HomeTabFragment) return; + + list.addOnScrollListener(new RecyclerView.OnScrollListener(){ + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){ + View topChild=recyclerView.getChildAt(0); + int firstChildPos=recyclerView.getChildAdapterPosition(topChild); + float newAlpha=firstChildPos>0 ? 1f : Math.min(1f, -topChild.getTop()/(float)headerTitle.getHeight()); + toolbarTitleView.setAlpha(newAlpha); + boolean newToolbarVisibility=newAlpha>0.5f; + if(newToolbarVisibility!=toolbarContentVisible){ + toolbarContentVisible=newToolbarVisibility; + createOptionsMenu(); + } + } + }); + } + @Override public boolean onFabLongClick(View v) { return UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtag+' '); @@ -252,6 +214,204 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment { @Override public Uri getWebUri(Uri.Builder base) { - return base.path((isInstanceAkkoma() ? "/tag/" : "/tags") + hashtag).build(); + return base.path((isInstanceAkkoma() ? "/tag/" : "/tags/") + hashtag).build(); + } + + @Override + protected RecyclerView.Adapter getAdapter(){ + View header=getActivity().getLayoutInflater().inflate(R.layout.header_hashtag_timeline, list, false); + headerTitle=header.findViewById(R.id.title); + headerSubtitle=header.findViewById(R.id.subtitle); + followButton=header.findViewById(R.id.profile_action_btn); + followProgress=header.findViewById(R.id.action_progress); + + headerTitle.setText("#"+hashtagName); + followButton.setVisibility(View.GONE); + followButton.setOnClickListener(v->{ + if(hashtag==null) + return; + setFollowed(!hashtag.following); + }); + followButton.setOnLongClickListener(v->{ + if(hashtag==null) return false; + UiUtils.pickAccount(getActivity(), accountID, R.string.sk_follow_as, R.drawable.ic_fluent_person_add_28_regular, session -> { + new SetTagFollowed(hashtagName, true).setCallback(new Callback<>(){ + @Override + public void onSuccess(Hashtag hashtag) { + Toast.makeText( + getActivity(), + getString(R.string.sk_followed_as, session.self.getShortUsername()), + Toast.LENGTH_SHORT + ).show(); + } + + @Override + public void onError(ErrorResponse error) { + error.showToast(getActivity()); + } + }).exec(session.getID()); + }, null); + return true; + }); + updateHeader(); + + MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter(); + if(!(getParentFragment() instanceof HomeTabFragment)){ + mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(header)); + } + mergeAdapter.addAdapter(super.getAdapter()); + return mergeAdapter; + } + + @Override + protected int getMainAdapterOffset(){ + return 1; + } + + private void createOptionsMenu(){ + optionsMenu.clear(); + optionsMenuInflater.inflate(R.menu.hashtag_timeline, optionsMenu); + followMenuItem=optionsMenu.findItem(R.id.follow_hashtag); + pinMenuItem=optionsMenu.findItem(R.id.pin); + followMenuItem.setVisible(toolbarContentVisible); + followMenuItem.setTitle(getString(hashtag.following ? R.string.unfollow_user : R.string.follow_user, "#"+hashtagName)); + followMenuItem.setIcon(hashtag.following ? R.drawable.ic_fluent_person_delete_24_filled : R.drawable.ic_fluent_person_add_24_regular); + pinMenuItem.setShowAsAction(toolbarContentVisible ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_ALWAYS); + super.updatePinButton(pinMenuItem); + if(toolbarContentVisible){ + UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu); + }else{ + UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu, R.id.pin); + } + } + + @Override + public void updatePinButton(MenuItem pin){ + super.updatePinButton(pin); + if(toolbarContentVisible) UiUtils.insetPopupMenuIcon(getContext(), pin); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ + inflater.inflate(R.menu.hashtag_timeline, menu); + super.onCreateOptionsMenu(menu, inflater); + optionsMenu=menu; + optionsMenuInflater=inflater; + createOptionsMenu(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item){ + if (super.onOptionsItemSelected(item)) return true; + if (item.getItemId() == R.id.follow_hashtag && hashtag!=null) { + setFollowed(!hashtag.following); + } + return true; + } + + @Override + protected void onUpdateToolbar(){ + super.onUpdateToolbar(); + toolbarTitleView.setAlpha(toolbarContentVisible ? 1f : 0f); + createOptionsMenu(); + } + + private void updateHeader(){ + if(hashtag==null) + return; + + if(hashtag.history!=null && !hashtag.history.isEmpty()){ + int weekPosts=hashtag.history.stream().mapToInt(h->h.uses).sum(); + int todayPosts=hashtag.history.get(0).uses; + int numAccounts=hashtag.history.stream().mapToInt(h->h.accounts).sum(); + int hSpace=V.dp(8); + SpannableStringBuilder ssb=new SpannableStringBuilder(); + ssb.append(getResources().getQuantityString(R.plurals.x_posts, weekPosts, weekPosts)); + ssb.append(" ", new SpacerSpan(hSpace, 0), 0); + ssb.append('·'); + ssb.append(" ", new SpacerSpan(hSpace, 0), 0); + ssb.append(getResources().getQuantityString(R.plurals.x_participants, numAccounts, numAccounts)); + ssb.append(" ", new SpacerSpan(hSpace, 0), 0); + ssb.append('·'); + ssb.append(" ", new SpacerSpan(hSpace, 0), 0); + ssb.append(getResources().getQuantityString(R.plurals.x_posts_today, todayPosts, todayPosts)); + headerSubtitle.setText(ssb); + } + + int styleRes; + followButton.setVisibility(View.VISIBLE); + if(hashtag.following){ + followButton.setText(R.string.button_following); + styleRes=R.style.Widget_Mastodon_M3_Button_Tonal; + }else{ + followButton.setText(R.string.button_follow); + styleRes=R.style.Widget_Mastodon_M3_Button_Filled; + } + TypedArray ta=followButton.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background}); + followButton.setBackground(ta.getDrawable(0)); + ta.recycle(); + ta=followButton.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor}); + followButton.setTextColor(ta.getColorStateList(0)); + followProgress.setIndeterminateTintList(ta.getColorStateList(0)); + ta.recycle(); + + followButton.setTextVisible(true); + followProgress.setVisibility(View.GONE); + if(followMenuItem!=null){ + followMenuItem.setTitle(getString(hashtag.following ? R.string.unfollow_user : R.string.follow_user, "#"+hashtagName)); + followMenuItem.setIcon(hashtag.following ? R.drawable.ic_fluent_person_delete_24_filled : R.drawable.ic_fluent_person_add_24_regular); + UiUtils.insetPopupMenuIcon(getContext(), followMenuItem); + } + } + + private void reloadTag(){ + new GetTag(hashtagName) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(Hashtag result){ + hashtag=result; + updateHeader(); + } + + @Override + public void onError(ErrorResponse error){ + + } + }) + .exec(accountID); + } + + private void setFollowed(boolean followed){ + if(followRequestRunning) + return; + getToolbar().performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK); + followButton.setTextVisible(false); + followProgress.setVisibility(View.VISIBLE); + followRequestRunning=true; + new SetTagFollowed(hashtagName, followed) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(Hashtag result){ + if(getActivity()==null) + return; + hashtag=result; + updateHeader(); + followRequestRunning=false; + } + + @Override + public void onError(ErrorResponse error){ + if(getActivity()==null) + return; + if(error instanceof MastodonErrorResponse er && "Duplicate record".equals(er.error)){ + hashtag.following=true; + }else{ + error.showToast(getActivity()); + } + updateHeader(); + followRequestRunning=false; + } + }) + .exec(accountID); } } 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 89e089bfb..19c7951ee 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java @@ -528,9 +528,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab if (list.repliesPolicy != null) args.putInt("repliesPolicy", list.repliesPolicy.ordinal()); Nav.go(getActivity(), ListTimelineFragment.class, args); } else if ((hashtag = hashtagsItems.get(id)) != null) { - args.putString("hashtag", hashtag.name); - args.putBoolean("following", hashtag.following); - Nav.go(getActivity(), HashtagTimelineFragment.class, args); + UiUtils.openHashtagTimeline(getContext(), accountID, hashtag); } return true; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java index 3c430f8b5..5afb46c56 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java @@ -369,6 +369,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList tabbar.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary)); tabbar.setTabTextSize(V.dp(14)); + tabLayoutMediator=new TabLayoutMediator(tabbar, pager, (tab, position)->tab.setText(switch(position){ + case 0 -> R.string.profile_featured; + case 1 -> R.string.profile_timeline; + case 2 -> R.string.profile_about; + default -> throw new IllegalStateException(); + })); tabLayoutMediator=new TabLayoutMediator(tabbar, pager, new TabLayoutMediator.TabConfigurationStrategy(){ @Override public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){ @@ -755,7 +761,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList followingCount.setText(UiUtils.abbreviateNumber(account.followingCount)); followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, account.followersCount))); followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, account.followingCount))); - + if (account.followersCount < 0) followersBtn.setVisibility(View.GONE); if (account.followingCount < 0) followingBtn.setVisibility(View.GONE); if (account.followersCount < 0 || account.followingCount < 0) diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java index 0cc8639a9..0a2249fb6 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java @@ -28,6 +28,7 @@ import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem; +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.displayitems.WarningFilteredStatusDisplayItem; @@ -81,7 +82,7 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist } @Subscribe - public void onStatusMuteChaged(StatusMuteChangedEvent ev){ + public void onStatusMuteChanged(StatusMuteChangedEvent ev){ for(Status s:data){ s.getContentStatus().update(ev); AccountSessionManager.get(accountID).getCacheController().updateStatus(s); @@ -131,6 +132,12 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist text.textSelectable=true; else if(item instanceof FooterStatusDisplayItem footer) footer.hideCounts=true; + else if(item instanceof SpoilerStatusDisplayItem spoiler){ + for(StatusDisplayItem subItem:spoiler.contentItems){ + if(subItem instanceof TextStatusDisplayItem text) + text.textSelectable=true; + } + } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java index f687ed7af..d502eed61 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java @@ -96,7 +96,7 @@ public class SearchFragment extends BaseStatusListFragment{ args.putParcelable("profileAccount", Parcels.wrap(res.account)); Nav.go(getActivity(), ProfileFragment.class, args); } - case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name, res.hashtag.following); + case HASHTAG -> UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name); case STATUS -> { Status status=res.status.getContentStatus(); Bundle args=new Bundle(); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchQueryFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchQueryFragment.java index 0f2a7a39d..c8b781dd4 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchQueryFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchQueryFragment.java @@ -378,7 +378,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment{ - UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name, res.hashtag.following); + UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag); AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putRecentSearch(res); }); } @@ -438,7 +438,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment{ onDataLoaded(List.of( new ListItem<>(R.string.settings_behavior, 0, R.drawable.ic_fluent_settings_24_regular, this::onBehaviorClick), new ListItem<>(R.string.settings_display, 0, R.drawable.ic_fluent_color_24_regular, this::onDisplayClick), -// new ListItem<>(R.string.settings_privacy, 0, R.drawable.ic_privacy_tip_24px, this::onPrivacyClick), + new ListItem<>(R.string.settings_privacy, 0, R.drawable.ic_privacy_tip_24px, this::onPrivacyClick), + new ListItem<>(R.string.settings_filters, 0, R.drawable.ic_filter_alt_24px, this::onFiltersClick), new ListItem<>(R.string.settings_notifications, 0, R.drawable.ic_fluent_alert_24_regular, this::onNotificationsClick), new ListItem<>(R.string.sk_settings_instance, 0, R.drawable.ic_fluent_server_24_regular, this::onInstanceClick), new ListItem<>(getString(R.string.about_app, getString(R.string.mo_app_name)), null, R.drawable.ic_fluent_info_24_regular, this::onAboutClick, null, 0, true), @@ -70,7 +71,9 @@ public class SettingsMainFragment extends BaseSettingsFragment{ data.add(0, new ListItem<>("Debug settings", null, R.drawable.ic_fluent_wrench_screwdriver_24_regular, ()->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true)); } - account.reloadPreferences(null); + AccountSession session=AccountSessionManager.get(accountID); + session.reloadPreferences(null); + session.updateAccountInfo(); E.register(this); } @@ -134,9 +137,9 @@ public class SettingsMainFragment extends BaseSettingsFragment{ Nav.go(getActivity(), SettingsDisplayFragment.class, makeFragmentArgs()); } -// private void onPrivacyClick(){ -// Nav.go(getActivity(), SettingsPrivacyFragment.class, makeFragmentArgs()); -// } + private void onPrivacyClick(){ + Nav.go(getActivity(), SettingsPrivacyFragment.class, makeFragmentArgs()); + } private void onFiltersClick(){ Nav.go(getActivity(), SettingsFiltersFragment.class, makeFragmentArgs()); 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 f41e0c91d..40c6a3df0 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Status.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Status.java @@ -7,8 +7,21 @@ import androidx.annotation.Nullable; import android.text.TextUtils; +import org.joinmastodon.android.api.ObjectValidationException; +import org.joinmastodon.android.api.RequiredField; +import org.joinmastodon.android.events.StatusCountersUpdatedEvent; +import org.joinmastodon.android.ui.text.HtmlParser; +import org.parceler.Parcel; + +import java.time.Instant; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + import androidx.annotation.NonNull; +import com.github.bottomSoftwareFoundation.bottom.Bottom; +import com.github.bottomSoftwareFoundation.bottom.TranslationError; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; @@ -17,17 +30,22 @@ import com.google.gson.JsonParseException; import org.joinmastodon.android.api.ObjectValidationException; import org.joinmastodon.android.api.RequiredField; +import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent; import org.joinmastodon.android.events.StatusCountersUpdatedEvent; import org.joinmastodon.android.events.StatusMuteChangedEvent; import org.joinmastodon.android.ui.text.HtmlParser; +import org.joinmastodon.android.utils.StatusTextEncoder; import org.parceler.Parcel; import java.lang.reflect.Type; import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.regex.Pattern; @Parcel public class Status extends BaseModel implements DisplayItemsParent, Searchable{ @@ -88,9 +106,9 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{ public transient boolean sensitiveRevealed; public transient boolean textExpanded, textExpandable; public transient boolean hasGapAfter; - public transient TranslatedStatus translation; - public transient boolean translationShown; private transient String strippedText; + public transient TranslationState translationState=TranslationState.HIDDEN; + public transient Translation translation; public Status(){} @@ -210,6 +228,38 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{ return (Status) super.clone(); } + 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 boolean isEligibleForTranslation(AccountSession session){ + Instance instanceInfo = AccountSessionManager.getInstance().getInstanceInfo(session.domain); + boolean translateEnabled = instanceInfo != null && + instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null && + instanceInfo.v2.configuration.translation.enabled; + + try { + String bottomText = BOTTOM_TEXT_PATTERN.matcher(getStrippedText()).find() + ? new StatusTextEncoder(Bottom::decode).decode(getStrippedText(), BOTTOM_TEXT_PATTERN) + : null; + if(bottomText==null || bottomText.length()==0 || bottomText.equals("\u0005")) bottomText=null; + if(bottomText!=null){ + translation=new Translation(); + translation.content=bottomText; + translation.detectedSourceLanguage="\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48"; + translation.provider="bottom-java"; + return true; + } + } catch (TranslationError ignored) {} + + return translateEnabled && !TextUtils.isEmpty(content) && !TextUtils.isEmpty(language) + && !Objects.equals(Locale.getDefault().getLanguage(), language) + && (visibility==StatusPrivacy.PUBLIC || visibility==StatusPrivacy.UNLISTED); + } + + public enum TranslationState{ + HIDDEN, + SHOWN, + LOADING + } + public boolean isReblogPermitted(String accountID){ return visibility.isReblogPermitted(account.id.equals( AccountSessionManager.getInstance().getAccount(accountID).self.id diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/TimelineDefinition.java b/mastodon/src/main/java/org/joinmastodon/android/model/TimelineDefinition.java index f63307e73..ac896d5b1 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/TimelineDefinition.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/TimelineDefinition.java @@ -233,7 +233,7 @@ public class TimelineDefinition { args.putString("listID", listId); args.putBoolean("listIsExclusive", listIsExclusive); } else if (type == TimelineType.HASHTAG) { - args.putString("hashtag", hashtagName); + args.putString("hashtagName", hashtagName); args.putBoolean("localOnly", hashtagLocalOnly); args.putStringArrayList("any", hashtagAny == null ? new ArrayList<>() : new ArrayList<>(hashtagAny)); args.putStringArrayList("all", hashtagAll == null ? new ArrayList<>() : new ArrayList<>(hashtagAll)); diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/TranslatedStatus.java b/mastodon/src/main/java/org/joinmastodon/android/model/TranslatedStatus.java deleted file mode 100644 index 44be68230..000000000 --- a/mastodon/src/main/java/org/joinmastodon/android/model/TranslatedStatus.java +++ /dev/null @@ -1,7 +0,0 @@ -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/StatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java index 21f9e7f5f..5763bf203 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java @@ -185,10 +185,8 @@ public abstract class StatusDisplayItem{ .ifPresent(hashtag -> items.add(new ReblogOrReplyLineStatusDisplayItem( parentID, fragment, hashtag.name, List.of(), R.drawable.ic_fluent_number_symbol_20sp_filled, null, - i -> { - args.putString("hashtag", hashtag.name); - Nav.go(fragment.getActivity(), HashtagTimelineFragment.class, args); - }, status + i->UiUtils.openHashtagTimeline(fragment.getActivity(), accountID, hashtag), + status ))); } 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 2cdaf66b1..19dc38d6b 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 @@ -1,36 +1,30 @@ package org.joinmastodon.android.ui.displayitems; +import static org.joinmastodon.android.ui.utils.UiUtils.opacityIn; + import android.app.Activity; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; -import android.text.TextUtils; import android.view.View; import android.view.ViewGroup; +import android.view.ViewStub; import android.widget.Button; import android.widget.LinearLayout; +import android.widget.ProgressBar; import android.widget.ScrollView; 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; -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.fragments.ThreadFragment; -import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.model.Translation; 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.StatusTextEncoder; import java.util.Locale; import java.util.regex.Pattern; @@ -40,32 +34,24 @@ 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; -import me.grishka.appkit.utils.CubicBezierInterpolator; 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 boolean disableTranslate; public final Status status; - public boolean disableTranslate, translationShown; - 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); this.text=text; this.status=status; this.disableTranslate=disableTranslate; - this.translationShown=status.translationShown; emojiHelper.setText(text); - session = AccountSessionManager.getInstance().getAccount(parentFragment.getAccountID()); - } - - public void setTranslationShown(boolean translationShown) { - this.translationShown = translationShown; - status.translationShown = translationShown; } @Override @@ -75,38 +61,47 @@ 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 TextView translateInfo, readMore; - private final View textWrap, translateWrap, translateProgress; - private final Button translateButton; - private final ScrollView textScrollView; + private final ViewStub translationFooterStub; + private View translationFooter, translationButtonWrap; + private TextView translationInfo; + private Button translationButton; + private ProgressBar translationProgress; - private final float textMaxHeight, textCollapsedHeight; + private final float textMaxHeight; private final LinearLayout.LayoutParams collapseParams, wrapParams; private final ViewGroup parent; + private final TextView readMore; + private final ScrollView textScrollView; public Holder(Activity activity, ViewGroup parent){ super(activity, R.layout.display_item_text, parent); this.parent=parent; text=findViewById(R.id.text); - textWrap = (LinearLayout) itemView; - translateWrap=findViewById(R.id.translate_wrap); - translateButton=findViewById(R.id.translate_btn); - translateInfo=findViewById(R.id.translate_info); - translateProgress=findViewById(R.id.translate_progress); + translationFooterStub=findViewById(R.id.translation_info); textScrollView=findViewById(R.id.text_scroll_view); readMore=findViewById(R.id.read_more); textMaxHeight=activity.getResources().getDimension(R.dimen.text_max_height); - textCollapsedHeight=activity.getResources().getDimension(R.dimen.text_collapsed_height); + float textCollapsedHeight=activity.getResources().getDimension(R.dimen.text_collapsed_height); collapseParams=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, (int) textCollapsedHeight); wrapParams=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); readMore.setOnClickListener(v -> item.parentFragment.onToggleExpanded(item.status, getItemID())); @@ -114,73 +109,20 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ @Override public void onBind(TextStatusDisplayItem item){ - boolean hasSpoiler = !TextUtils.isEmpty(item.status.spoilerText); - text.setText(item.translationShown - ? HtmlParser.parse(item.status.translation.content, item.status.emojis, item.status.mentions, item.status.tags, item.parentFragment.getAccountID()) - : item.text); - text.setTextIsSelectable(item.textSelectable); - if (item.textSelectable) { - textScrollView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + 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); itemView.setPadding(itemView.getPaddingLeft(), item.reduceTopPadding ? V.dp(6) : V.dp(12), itemView.getPaddingRight(), itemView.getPaddingBottom()); text.setTextColor(UiUtils.getThemeColor(text.getContext(), item.inset ? R.attr.colorM3OnSurfaceVariant : R.attr.colorM3OnSurface)); - - Instance instanceInfo = AccountSessionManager.getInstance().getInstanceInfo(item.session.domain); - boolean translateEnabled = !item.disableTranslate && instanceInfo != null && - instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null && - instanceInfo.v2.configuration.translation.enabled; - String bottomText = null; - try { - bottomText = BOTTOM_TEXT_PATTERN.matcher(item.status.getStrippedText()).find() - ? new StatusTextEncoder(Bottom::decode).decode(item.status.getStrippedText(), BOTTOM_TEXT_PATTERN) - : null; - } catch (TranslationError ignored) {} - - boolean translateVisible = (bottomText != null || ( - translateEnabled && - !item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) && - item.status.language != null && - // todo: compare to mastodon locale instead (how do i query that?!) - !item.status.language.equalsIgnoreCase(Locale.getDefault().getLanguage()))) - && (!GlobalUserPreferences.translateButtonOpenedOnly || item.textSelectable); - translateWrap.setVisibility(translateVisible ? View.VISIBLE : View.GONE); - translateButton.setText(item.translationShown ? R.string.sk_translate_show_original : R.string.sk_translate_post); - translateInfo.setText(item.translationShown ? itemView.getResources().getString(R.string.sk_translated_using, bottomText != null ? "bottom-java" : item.status.translation.provider) : ""); - String finalBottomText = bottomText; - translateButton.setOnClickListener(v->{ - if (item.status.translation == null) { - if (finalBottomText != null) { - try { - item.status.translation = new TranslatedStatus(); - item.status.translation.content = finalBottomText; - item.setTranslationShown(true); - } catch (TranslationError err) { - item.status.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(); - - if(item.status.isRemote){ - UiUtils.lookupStatus(item.parentFragment.getContext(), - item.status, - item.parentFragment.getAccountID(), - null, - reloadedStatus -> loadTranslation(reloadedStatus.id)); - } else { - loadTranslation(item.status.id); - } - } else { - item.setTranslationShown(!item.translationShown); - rebind(); - } - }); + updateTranslation(false); readMore.setText(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand); @@ -214,18 +156,19 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ if (GlobalUserPreferences.collapseLongPosts && !item.status.textExpandable) { boolean tooBig = text.getMeasuredHeight() > textMaxHeight; - boolean expandable = tooBig && !hasSpoiler; + boolean expandable = tooBig && !item.status.hasSpoiler(); item.parentFragment.onEnableExpandable(Holder.this, expandable); } boolean expandButtonShown=item.status.textExpandable && !item.status.textExpanded; - translateWrap.setPadding(0, V.dp(expandButtonShown ? 0 : 4), 0, 0); + if(translationFooter!=null) + translationFooter.setPadding(0, V.dp(expandButtonShown ? 0 : 4), 0, 0); readMore.setVisibility(expandButtonShown ? View.VISIBLE : View.GONE); textScrollView.setLayoutParams(item.status.textExpandable && !item.status.textExpanded ? collapseParams : wrapParams); // compensate for spoiler's bottom margin ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams(); - params.setMargins(params.leftMargin, item.inset && hasSpoiler ? V.dp(-16) : 0, + params.setMargins(params.leftMargin, item.inset && item.status.hasSpoiler() ? V.dp(-16) : 0, params.rightMargin, params.bottomMargin); } @@ -250,28 +193,57 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ return item.emojiHelper; } - private void loadTranslation(String statusId) { - new TranslateStatus(statusId).setCallback(new Callback<>() { - @Override - public void onSuccess(TranslatedStatus translatedStatus) { - item.status.translation = translatedStatus; - item.setTranslationShown(true); - if (item.parentFragment.getActivity() == null) return; - translateProgress.setVisibility(View.GONE); - translateButton.setClickable(true); - translateButton.animate().alpha(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(50).start(); - rebind(); + public void updateTranslation(boolean updateText){ + if(item.status==null) + return; + boolean translateEnabled=!item.disableTranslate && item.status.isEligibleForTranslation(item.parentFragment.getSession()); + if(translationFooter==null && translateEnabled){ + translationFooter=translationFooterStub.inflate(); + translationInfo=findViewById(R.id.translation_info_text); + translationButton=findViewById(R.id.translation_btn); + translationButtonWrap=findViewById(R.id.translation_btn_wrap); + translationProgress=findViewById(R.id.translation_progress); + translationButton.setOnClickListener(v->item.parentFragment.togglePostTranslation(item.status, item.parentID)); + } + if(item.status.translationState==Status.TranslationState.HIDDEN){ + if(updateText) text.setText(item.text); + if(translationFooter==null) return; + translationFooter.setVisibility(translateEnabled ? View.VISIBLE : View.GONE); + translationProgress.setVisibility(View.GONE); + Translation existingTrans=item.status.getContentStatus().translation; + String lang=existingTrans!=null ? existingTrans.detectedSourceLanguage : null; + String displayLang=Locale.forLanguageTag(lang!=null ? lang : item.status.getContentStatus().language).getDisplayLanguage(); + translationButton.setText(item.parentFragment.getString(R.string.translate_post, !displayLang.isBlank() ? displayLang : lang)); + translationButton.setEnabled(true); + translationButton.setAlpha(1); + translationInfo.setVisibility(View.GONE); + UiUtils.beginLayoutTransition((ViewGroup) translationButtonWrap); + }else{ + translationFooter.setVisibility(View.VISIBLE); + if(item.status.translationState==Status.TranslationState.SHOWN){ + translationProgress.setVisibility(View.GONE); + translationButton.setText(R.string.translation_show_original); + translationButton.setEnabled(true); + translationButton.setAlpha(1); + translationInfo.setVisibility(View.VISIBLE); + translationButton.setVisibility(View.VISIBLE); + String displayLang=Locale.forLanguageTag(item.status.translation.detectedSourceLanguage).getDisplayLanguage(); + translationInfo.setText(translationInfo.getContext().getString(R.string.post_translated, !displayLang.isBlank() ? displayLang : item.status.translation.detectedSourceLanguage, item.status.translation.provider)); + UiUtils.beginLayoutTransition((ViewGroup) translationButtonWrap); + if(updateText){ + if(item.translatedText==null){ + item.setTranslatedText(item.status.translation.content); + } + text.setText(item.translatedText); + } + }else{ // LOADING + translationProgress.setVisibility(View.VISIBLE); + translationButton.setEnabled(false); + translationButton.startAnimation(opacityIn); + translationInfo.setVisibility(View.INVISIBLE); + UiUtils.beginLayoutTransition((ViewGroup) translationButton.getParent()); } - - @Override - public void onError(ErrorResponse error) { - translateProgress.setVisibility(View.GONE); - translateButton.setClickable(true); - translateButton.animate().alpha(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(50).start(); - error.showToast(itemView.getContext()); - } - }).exec(item.parentFragment.getAccountID()); - + } } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java index 93af682b0..fd2c00e70 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java @@ -103,6 +103,7 @@ public class HtmlParser{ Map idsByUrl=mentions.stream().filter(mention -> mention.id != null).collect(Collectors.toMap(m->m.url, m->m.id)); // Hashtags in remote posts have remote URLs, these have local URLs so they don't match. // Map tagsByUrl=tags.stream().collect(Collectors.toMap(t->t.url, t->t.name)); + Map tagsByTag=tags.stream().distinct().collect(Collectors.toMap(t->t.name.toLowerCase(), Function.identity())); final SpannableStringBuilder ssb=new SpannableStringBuilder(); Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){ @@ -115,6 +116,7 @@ public class HtmlParser{ }else if(node instanceof Element el){ switch(el.nodeName()){ case "a" -> { + Object linkObject=null; String href=el.attr("href"); LinkSpan.Type linkType; String text=el.text(); @@ -122,6 +124,7 @@ public class HtmlParser{ if(text.startsWith("#")){ linkType=LinkSpan.Type.HASHTAG; href=text.substring(1); + linkObject=tagsByTag.get(text.substring(1).toLowerCase()); }else{ linkType=LinkSpan.Type.URL; } @@ -136,7 +139,7 @@ public class HtmlParser{ }else{ linkType=LinkSpan.Type.URL; } - openSpans.add(new SpanInfo(new LinkSpan(href, null, linkType, accountID), ssb.length(), el)); + openSpans.add(new SpanInfo(new LinkSpan(href, null, linkType, accountID, linkObject, text), ssb.length(), el)); } case "br" -> ssb.append('\n'); case "span" -> { @@ -271,7 +274,7 @@ public class HtmlParser{ String url=matcher.group(3); if(TextUtils.isEmpty(matcher.group(4))) url="http://"+url; - ssb.setSpan(new LinkSpan(url, null, LinkSpan.Type.URL, null), matcher.start(3), matcher.end(3), 0); + ssb.setSpan(new LinkSpan(url, null, LinkSpan.Type.URL, null, null, null), matcher.start(3), matcher.end(3), 0); }while(matcher.find()); // Find more URLs return ssb; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java index 33e34894e..4689a8fdd 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java @@ -5,6 +5,7 @@ import android.text.TextPaint; import android.text.style.CharacterStyle; import android.view.View; +import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.ui.utils.UiUtils; public class LinkSpan extends CharacterStyle { @@ -14,17 +15,15 @@ public class LinkSpan extends CharacterStyle { private String link; private Type type; private String accountID; + private Object linkObject; private String text; - public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID){ - this(link, listener, type, accountID, null); - } - - public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID, String text){ + public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID, Object linkObject, String text){ this.listener=listener; this.link=link; this.type=type; this.accountID=accountID; + this.linkObject=linkObject; this.text=text; } @@ -37,12 +36,17 @@ public class LinkSpan extends CharacterStyle { tp.setColor(color=tp.linkColor); tp.setUnderlineText(true); } - + public void onClick(Context context){ switch(getType()){ case URL -> UiUtils.openURL(context, accountID, link); case MENTION -> UiUtils.openProfileByID(context, accountID, link); - case HASHTAG -> UiUtils.openHashtagTimeline(context, accountID, link, null); + case HASHTAG -> { + if(linkObject instanceof Hashtag ht) + UiUtils.openHashtagTimeline(context, accountID, ht); + else + UiUtils.openHashtagTimeline(context, accountID, text); + } case CUSTOM -> listener.onLinkClick(this); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index 0cbf78317..cd5b18b6d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -106,6 +106,7 @@ import org.joinmastodon.android.model.AccountField; import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Notification; +import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.ScheduledStatus; import org.joinmastodon.android.model.SearchResults; @@ -219,20 +220,11 @@ public class UiUtils { if(diff<1000L){ return context.getString(R.string.time_now); }else if(diff<60_000L){ - long time = diff/1000L; - return ago ? - context.getString(R.string.time_seconds_ago_short, time) : - context.getResources().getQuantityString(R.plurals.sk_time_seconds, (int) time, time); + return context.getString(ago ? R.string.time_seconds_ago_short : R.string.sk_time_seconds, diff/1000L); }else if(diff<3600_000L){ - long time = diff/60_000L; - return ago ? - context.getString(R.string.time_minutes_ago_short, time) : - context.getResources().getQuantityString(R.plurals.sk_time_minutes, (int) time, time); + return context.getString(ago ? R.string.time_minutes_ago_short : R.string.sk_time_minutes, diff/60_000L); }else if(diff<3600_000L*24L){ - long time = diff/3600_000L; - return ago ? - context.getString(R.string.time_hours_ago_short, time) : - context.getResources().getQuantityString(R.plurals.sk_time_hours, (int) time, time); + return context.getString(ago ? R.string.time_hours_ago_short : R.string.sk_time_hours, diff/3600_000L); } else { int days = (int) (diff / (3600_000L * 24L)); if (ago && days > 30) { @@ -243,7 +235,7 @@ public class UiUtils { return DATE_FORMATTER_SHORT_WITH_YEAR.format(dt); } } - return ago ? context.getString(R.string.time_days_ago_short, days) : context.getResources().getQuantityString(R.plurals.sk_time_days, days, days); + return context.getString(ago ? R.string.time_days_ago_short : R.string.sk_time_days, days); } } @@ -456,12 +448,18 @@ public class UiUtils { Nav.go((Activity) context, ProfileFragment.class, args); } - public static void openHashtagTimeline(Context context, String accountID, String hashtag, @Nullable Boolean following) { - Bundle args = new Bundle(); + public static void openHashtagTimeline(Context context, String accountID, Hashtag hashtag){ + Bundle args=new Bundle(); args.putString("account", accountID); - args.putString("hashtag", hashtag); - if (following != null) args.putBoolean("following", following); - Nav.go((Activity) context, HashtagTimelineFragment.class, args); + args.putParcelable("hashtag", Parcels.wrap(hashtag)); + Nav.go((Activity)context, HashtagTimelineFragment.class, args); + } + + public static void openHashtagTimeline(Context context, String accountID, String hashtag){ + Bundle args=new Bundle(); + args.putString("account", accountID); + args.putString("hashtagName", hashtag); + Nav.go((Activity)context, HashtagTimelineFragment.class, args); } public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, Runnable onConfirmed) { @@ -1386,7 +1384,7 @@ public class UiUtils { } }) .execNoAuth(uri.getHost())); - } else if (looksLikeMastodonUrl(url)) { + } else if (looksLikeFediverseUrl(url)) { return Optional.of(new GetSearchResults(url, null, true, null, 0, 0) .setCallback(new Callback<>() { @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ComposePollViewController.java b/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ComposePollViewController.java index 9281ed304..3311ce418 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ComposePollViewController.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ComposePollViewController.java @@ -75,15 +75,15 @@ public class ComposePollViewController{ Instance instance=fragment.instance; if (!instance.isAkkoma()) { - if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0) - maxPollOptions=instance.configuration.polls.maxOptions; - if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0) - maxPollOptionLength=instance.configuration.polls.maxCharactersPerOption; - } else { if(instance!=null && instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0) maxPollOptions=instance.configuration.polls.maxOptions; if(instance!=null && instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0) maxPollOptionLength=instance.configuration.polls.maxCharactersPerOption; + } else { + if(instance!=null && instance.pollLimits!=null && instance.pollLimits.maxOptions>0) + maxPollOptions=instance.pollLimits.maxOptions; + if(instance!=null && instance.pollLimits!=null && instance.pollLimits.maxOptionChars>0) + maxPollOptionLength=instance.pollLimits.maxOptionChars; } pollOptionsView=pollWrap.findViewById(R.id.poll_options); diff --git a/mastodon/src/main/res/layout/display_item_text.xml b/mastodon/src/main/res/layout/display_item_text.xml index 8729b9886..145bd56ef 100644 --- a/mastodon/src/main/res/layout/display_item_text.xml +++ b/mastodon/src/main/res/layout/display_item_text.xml @@ -45,56 +45,10 @@ android:visibility="gone" android:importantForAccessibility="no"/> - - - - - - - - - - - - + android:layout="@layout/footer_text_translation"/> diff --git a/mastodon/src/main/res/layout/footer_text_translation.xml b/mastodon/src/main/res/layout/footer_text_translation.xml index b80e730f8..22f85cc73 100644 --- a/mastodon/src/main/res/layout/footer_text_translation.xml +++ b/mastodon/src/main/res/layout/footer_text_translation.xml @@ -1,44 +1,50 @@ - + android:layout_height="wrap_content" + android:paddingTop="6dp" + android:gravity="center_vertical"> - - - - - - -