diff --git a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java index f9f8a63a8..c338d7561 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java +++ b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java @@ -46,6 +46,7 @@ public class GlobalUserPreferences{ public static boolean bottomEncoding; public static boolean collapseLongPosts; public static boolean spectatorMode; + public static boolean autoHideFab; public static String publishButtonText; public static ThemePreference theme; public static ColorPreference color; @@ -99,6 +100,7 @@ public class GlobalUserPreferences{ bottomEncoding=prefs.getBoolean("bottomEncoding", false); collapseLongPosts=prefs.getBoolean("collapseLongPosts", true); spectatorMode=prefs.getBoolean("spectatorMode", false); + autoHideFab=prefs.getBoolean("autoHideFab", true); publishButtonText=prefs.getString("publishButtonText", ""); theme=ThemePreference.values()[prefs.getInt("theme", 0)]; recentLanguages=fromJson(prefs.getString("recentLanguages", "{}"), recentLanguagesType, new HashMap<>()); @@ -147,6 +149,7 @@ public class GlobalUserPreferences{ .putBoolean("prefixRepliesWithRe", prefixRepliesWithRe) .putBoolean("collapseLongPosts", collapseLongPosts) .putBoolean("spectatorMode", spectatorMode) + .putBoolean("autoHideFab", autoHideFab) .putString("publishButtonText", publishButtonText) .putBoolean("bottomEncoding", bottomEncoding) .putInt("theme", theme.ordinal()) 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 71b331d2f..ed5094d5b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java @@ -26,6 +26,7 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; +import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; @@ -361,6 +362,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList }); actionButton.setOnClickListener(this::onActionButtonClick); + actionButton.setOnLongClickListener(this::onActionButtonLongClick); notifyButton.setOnClickListener(this::onNotifyButtonClick); avatar.setOnClickListener(this::onAvatarClick); cover.setOnClickListener(this::onCoverClick); @@ -576,8 +578,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList for (Account.Role role : account.roles) { TextView roleText = new TextView(getActivity(), null, 0, R.style.role_label); roleText.setText(role.name); - GradientDrawable bg = (GradientDrawable) roleText.getBackground().mutate(); - bg.setStroke(V.dp(2), Color.parseColor(role.color)); + if (!TextUtils.isEmpty(role.color) && role.color.startsWith("#")) try { + GradientDrawable bg = (GradientDrawable) roleText.getBackground().mutate(); + bg.setStroke(V.dp(2), Color.parseColor(role.color)); + } catch (Exception ignored) {} rolesView.addView(roleText); } } @@ -706,6 +710,16 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList }else{ UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.bookmarks, R.id.followed_hashtags, R.id.favorites, R.id.scheduled); } + boolean hasMultipleAccounts = AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1; + MenuItem openWithAccounts = menu.findItem(R.id.open_with_account); + openWithAccounts.setVisible(hasMultipleAccounts); + SubMenu accountsMenu = openWithAccounts.getSubMenu(); + if (hasMultipleAccounts) { + accountsMenu.clear(); + UiUtils.populateAccountsMenu(accountID, accountsMenu, s-> UiUtils.openURL( + getActivity(), s.getID(), account.url, false + )); + } menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getShortUsername())); if(isOwnProfile) return; @@ -880,33 +894,34 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList currentPhotoViewer.offsetView(0, oldScrollY-scrollY); } - int dy = scrollY - oldScrollY; - - if (dy > 0 && fab.getVisibility() == View.VISIBLE) { - TranslateAnimation animate = new TranslateAnimation( - 0, - 0, - 0, - fab.getHeight() * 2); - animate.setDuration(300); - animate.setFillAfter(true); - fab.startAnimation(animate); - fab.setVisibility(View.INVISIBLE); - scrollDiff = 0; - } else if (dy < 0 && fab.getVisibility() != View.VISIBLE) { - if (scrollDiff > 400) { - fab.setVisibility(View.VISIBLE); + if (GlobalUserPreferences.enableFabAutoHide) { + int dy = scrollY - oldScrollY; + if (dy > 0 && fab.getVisibility() == View.VISIBLE) { TranslateAnimation animate = new TranslateAnimation( 0, 0, - fab.getHeight() * 2, - 0); + 0, + fab.getHeight() * 2); animate.setDuration(300); animate.setFillAfter(true); fab.startAnimation(animate); + fab.setVisibility(View.INVISIBLE); scrollDiff = 0; - } else { - scrollDiff += Math.abs(dy); + } else if (dy < 0 && fab.getVisibility() != View.VISIBLE) { + if (v.getScrollY() == 0 || scrollDiff > 400) { + fab.setVisibility(View.VISIBLE); + TranslateAnimation animate = new TranslateAnimation( + 0, + 0, + fab.getHeight() * 2, + 0); + animate.setDuration(300); + animate.setFillAfter(true); + fab.startAnimation(animate); + scrollDiff = 0; + } else { + scrollDiff += Math.abs(dy); + } } } } @@ -937,6 +952,31 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList } } + private boolean onActionButtonLongClick(View v) { + if (isOwnProfile || AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false; + UiUtils.pickAccount(getActivity(), accountID, R.string.sk_follow_as, R.drawable.ic_fluent_person_add_28_regular, session -> { + UiUtils.lookupAccount(getActivity(), account, session.getID(), accountID, acc -> { + if (acc == null) return; + new SetAccountFollowed(acc.id, true, true).setCallback(new Callback<>() { + @Override + public void onSuccess(Relationship relationship) { + 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; + } + private void setActionProgressVisible(boolean visible){ actionButton.setTextVisible(!visible); actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE); diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Account.java b/mastodon/src/main/java/org/joinmastodon/android/model/Account.java index 9f996d769..362406f85 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Account.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Account.java @@ -14,7 +14,7 @@ import java.util.List; * Represents a user of Mastodon and their associated profile. */ @Parcel -public class Account extends BaseModel{ +public class Account extends BaseModel implements Searchable{ // Base attributes /** @@ -135,6 +135,11 @@ public class Account extends BaseModel{ public List roles; + @Override + public String getQuery() { + return url; + } + @Parcel public static class Role { public String name; 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 b1cc26252..09aae3adc 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Instance.java @@ -121,7 +121,7 @@ public class Instance extends BaseModel{ ci.domain=uri; ci.normalizedDomain=IDN.toUnicode(uri); ci.description=Html.fromHtml(shortDescription).toString().trim(); - if(languages!=null){ + if(languages!=null && languages.size() > 0){ ci.language=languages.get(0); ci.languages=languages; }else{ diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Searchable.java b/mastodon/src/main/java/org/joinmastodon/android/model/Searchable.java new file mode 100644 index 000000000..6580a3cea --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Searchable.java @@ -0,0 +1,5 @@ +package org.joinmastodon.android.model; + +public interface Searchable { + String getQuery(); +} 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 41d11348c..31cd25cf5 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Status.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Status.java @@ -11,7 +11,7 @@ import java.time.Instant; import java.util.List; @Parcel -public class Status extends BaseModel implements DisplayItemsParent{ +public class Status extends BaseModel implements DisplayItemsParent, Searchable{ @RequiredField public String id; @RequiredField @@ -163,4 +163,9 @@ public class Status extends BaseModel implements DisplayItemsParent{ s.filtered = List.of(); return s; } + + @Override + public String getQuery() { + return url; + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java index c5b2afe06..ca82fb8ea 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java @@ -205,6 +205,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{ String accountID = session.getID(); args.putString("account", accountID); UiUtils.lookupStatus(v.getContext(), item.status, accountID, item.accountID, status -> { + if (status == null) return; args.putParcelable("replyTo", Parcels.wrap(status)); Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args); }); 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 1fbbe7e59..5cad22da0 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 @@ -481,7 +481,9 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ if (hasMultipleAccounts && accountsMenu != null) { openWithAccounts.setVisible(true); accountsMenu.clear(); - populateAccountsMenu(accountsMenu); + UiUtils.populateAccountsMenu(item.accountID, accountsMenu, s-> UiUtils.openURL( + item.parentFragment.getActivity(), s.getID(), item.status.url, false + )); } else if (openWithAccounts != null) { openWithAccounts.setVisible(false); } 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 e3bbf46b5..2aa903867 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 @@ -87,6 +87,7 @@ import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.ScheduledStatus; import org.joinmastodon.android.model.SearchResults; +import org.joinmastodon.android.model.Searchable; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.StatusPrivacy; import org.joinmastodon.android.ui.M3AlertDialogBuilder; @@ -107,8 +108,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.BiPredicate; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -978,6 +981,8 @@ public class UiUtils{ public static void pickInteractAs(Context context, String accountID, Status sourceStatus, Predicate checkInteracted, InteractionPerformer interactionPerformer, @StringRes int interactAsRes, @StringRes int interactedAsAccountRes, @StringRes int alreadyInteractedRes, @DrawableRes int iconRes) { pickAccount(context, accountID, interactAsRes, iconRes, session -> { lookupStatus(context, sourceStatus, session.getID(), accountID, status -> { + if (status == null) return; + if (checkInteracted.test(status)) { Toast.makeText(context, alreadyInteractedRes, Toast.LENGTH_SHORT).show(); return; @@ -993,17 +998,33 @@ public class UiUtils{ }, null); } - public static void lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer statusConsumer) { + public static void lookupStatus(Context context, Status queryStatus, String targetAccountID, @Nullable String sourceAccountID, Consumer resultConsumer) { + lookup(context, queryStatus, targetAccountID, sourceAccountID, GetSearchResults.Type.STATUSES, resultConsumer, results -> + !results.statuses.isEmpty() ? Optional.of(results.statuses.get(0)) : Optional.empty() + ); + } + + public static void lookupAccount(Context context, Account queryAccount, String targetAccountID, @Nullable String sourceAccountID, Consumer resultConsumer) { + lookup(context, queryAccount, targetAccountID, sourceAccountID, GetSearchResults.Type.ACCOUNTS, resultConsumer, results -> + !results.accounts.isEmpty() ? Optional.of(results.accounts.get(0)) : Optional.empty() + ); + } + + public static void lookup(Context context, T query, String targetAccountID, @Nullable String sourceAccountID, @Nullable GetSearchResults.Type type, Consumer resultConsumer, Function> extractResult) { if (sourceAccountID != null && targetAccountID.startsWith(sourceAccountID.substring(0, sourceAccountID.indexOf('_')))) { - statusConsumer.accept(queryStatus); + resultConsumer.accept(query); return; } - new GetSearchResults(queryStatus.url, GetSearchResults.Type.STATUSES, true).setCallback(new Callback<>() { + new GetSearchResults(query.getQuery(), type, true).setCallback(new Callback<>() { @Override public void onSuccess(SearchResults results) { - if (!results.statuses.isEmpty()) statusConsumer.accept(results.statuses.get(0)); - else Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show(); + Optional result = extractResult.apply(results); + if (result.isPresent()) resultConsumer.accept(result.get()); + else { + Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show(); + resultConsumer.accept(null); + } } @Override @@ -1239,6 +1260,17 @@ public class UiUtils{ return intent; } + public static void populateAccountsMenu(String excludeAccountID, Menu menu, Consumer onClick) { + List sessions=AccountSessionManager.getInstance().getLoggedInAccounts(); + sessions.stream().filter(s -> !s.getID().equals(excludeAccountID)).forEach(s -> { + String username = "@"+s.self.username+"@"+s.domain; + menu.add(username).setOnMenuItemClickListener((c) -> { + onClick.accept(s); + return true; + }); + }); + } + /** * Wraps a View.OnClickListener to filter multiple clicks in succession. * Useful for buttons that perform some action that changes their state asynchronously. diff --git a/mastodon/src/main/res/drawable/bg_pill.xml b/mastodon/src/main/res/drawable/bg_pill.xml index af6a69a4f..d2318453f 100644 --- a/mastodon/src/main/res/drawable/bg_pill.xml +++ b/mastodon/src/main/res/drawable/bg_pill.xml @@ -4,5 +4,5 @@ android:shape="rectangle"> - + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_fluent_person_add_28_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_person_add_28_regular.xml new file mode 100644 index 000000000..5fa82eb93 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_person_add_28_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/menu/profile.xml b/mastodon/src/main/res/menu/profile.xml index ec14792fb..2e01cb95d 100644 --- a/mastodon/src/main/res/menu/profile.xml +++ b/mastodon/src/main/res/menu/profile.xml @@ -1,5 +1,8 @@ + + + diff --git a/mastodon/src/main/res/menu/profile_own.xml b/mastodon/src/main/res/menu/profile_own.xml index c050b82cb..02cf30e63 100644 --- a/mastodon/src/main/res/menu/profile_own.xml +++ b/mastodon/src/main/res/menu/profile_own.xml @@ -2,6 +2,9 @@ + + + diff --git a/mastodon/src/main/res/values-de-rDE/strings_sk.xml b/mastodon/src/main/res/values-de-rDE/strings_sk.xml index 477d33a85..83992076a 100644 --- a/mastodon/src/main/res/values-de-rDE/strings_sk.xml +++ b/mastodon/src/main/res/values-de-rDE/strings_sk.xml @@ -258,4 +258,7 @@ Anhänge richtig stellen\? Interaktions-Buttons verstecken Einige Anhänge sind nicht fertig hochgeladen. + Mit %s gefolgt + Verfassen-Button automatisch ausblenden + Mit anderem Account folgen \ No newline at end of file diff --git a/mastodon/src/main/res/values/strings_sk.xml b/mastodon/src/main/res/values/strings_sk.xml index ea65ed0bc..362a6ef1a 100644 --- a/mastodon/src/main/res/values/strings_sk.xml +++ b/mastodon/src/main/res/values/strings_sk.xml @@ -259,4 +259,7 @@ Fix attachments? Some attachments haven’t finished uploading. Hide interaction buttons + Follow from other account + Followed from %s + Auto-hide Compose button \ No newline at end of file diff --git a/mastodon/src/main/res/xml/locales_config.xml b/mastodon/src/main/res/xml/locales_config.xml index 6b94551e1..473134120 100644 --- a/mastodon/src/main/res/xml/locales_config.xml +++ b/mastodon/src/main/res/xml/locales_config.xml @@ -6,11 +6,13 @@ + + @@ -21,6 +23,7 @@ + @@ -28,7 +31,9 @@ + + diff --git a/metadata/de-DE/changelogs/77.txt b/metadata/de-DE/changelogs/77.txt new file mode 100644 index 000000000..d3d60b2c6 --- /dev/null +++ b/metadata/de-DE/changelogs/77.txt @@ -0,0 +1,4 @@ +- Folgen-Button lange drücken, um Profil von anderem Account zu folgen +- Option, um Profile mit anderem Account zu öffnen +- Verfassen-Button wird beim Scrollen automatisch ausgeblendet +- Crash beim Öffnen von Admin-Accounts behoben diff --git a/metadata/en-US/changelogs/76.txt b/metadata/en-US/changelogs/76.txt index 6b94016b3..a0a0dcf80 100644 --- a/metadata/en-US/changelogs/76.txt +++ b/metadata/en-US/changelogs/76.txt @@ -3,4 +3,4 @@ - Collapse/expand function for very long posts - Option to automatically prefix replys CWs with “re:” - Option to hide interaction buttons in the timeline -- Various bugfixes, tweaks and improvements \ No newline at end of file +- Various bugfixes, tweaks and improvements diff --git a/metadata/en-US/changelogs/77.txt b/metadata/en-US/changelogs/77.txt new file mode 100644 index 000000000..5ce83835c --- /dev/null +++ b/metadata/en-US/changelogs/77.txt @@ -0,0 +1,4 @@ +- Long-press follow button to follow profiles from other account +- Option to open profiles in other account +- Auto-hide compose button when scrolling down the timeline +- Fix crash when opening server admin's profiles