diff --git a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java index d82484069..b857c9fbf 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java +++ b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java @@ -12,8 +12,10 @@ import org.joinmastodon.android.model.TimelineDefinition; import java.lang.reflect.Type; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; public class GlobalUserPreferences{ public static boolean playGifs; @@ -45,6 +47,8 @@ public class GlobalUserPreferences{ private final static Type pinnedTimelinesType = new TypeToken>>() {}.getType(); public static Map> recentLanguages; public static Map> pinnedTimelines; + public static Set accountsWithLocalOnlySupport; + public static Set accountsInGlitchMode; private static SharedPreferences getPrefs(){ return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE); @@ -83,6 +87,8 @@ public class GlobalUserPreferences{ theme=ThemePreference.values()[prefs.getInt("theme", 0)]; recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new HashMap<>()); pinnedTimelines=fromJson(prefs.getString("pinnedTimelines", null), pinnedTimelinesType, new HashMap<>()); + accountsWithLocalOnlySupport=prefs.getStringSet("accountsWithLocalOnlySupport", new HashSet<>()); + accountsInGlitchMode=prefs.getStringSet("accountsInGlitchMode", new HashSet<>()); try { color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name())); @@ -119,6 +125,8 @@ public class GlobalUserPreferences{ .putString("color", color.name()) .putString("recentLanguages", gson.toJson(recentLanguages)) .putString("pinnedTimelines", gson.toJson(pinnedTimelines)) + .putStringSet("accountsWithLocalOnlySupport", accountsWithLocalOnlySupport) + .putStringSet("accountsInGlitchMode", accountsInGlitchMode) .apply(); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/CreateStatus.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/CreateStatus.java index 6a9c0cea1..bd6b307b1 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/CreateStatus.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/CreateStatus.java @@ -39,6 +39,7 @@ public class CreateStatus extends MastodonAPIRequest{ public Poll poll; public String inReplyToId; public boolean sensitive; + public boolean localOnly; public String spoilerText; public StatusPrivacy visibility; public Instant scheduledAt; 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 3c3a0731b..a923a09de 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java @@ -151,6 +151,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private static final int IMAGE_DESCRIPTION_RESULT=363; private static final int SCHEDULED_STATUS_OPENED_RESULT=161; private static final int MAX_ATTACHMENTS=4; + private static final String GLITCH_LOCAL_ONLY_SUFFIX = "๐Ÿ‘"; private static final String TAG="ComposeFragment"; private static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE); @@ -163,7 +164,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private final BreakIterator breakIterator=BreakIterator.getCharacterInstance(); private SizeListenerLinearLayout contentView; - private TextView selfName, selfUsername; + private TextView selfName, selfUsername, selfExtraText, extraText; private ImageView selfAvatar; private Account self; private String instanceDomain; @@ -212,6 +213,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private View sendingOverlay; private WindowManager wm; private StatusPrivacy statusVisibility=StatusPrivacy.PUBLIC; + private boolean localOnly; private ComposeAutocompleteSpan currentAutocompleteSpan; private FrameLayout mainEditTextWrap; private ComposeAutocompleteViewController autocompleteViewController; @@ -242,9 +244,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr instance=AccountSessionManager.getInstance().getInstanceInfo(instanceDomain); languageResolver=new MastodonLanguage.LanguageResolver(instance); redraftStatus=getArguments().getBoolean("redraftStatus", false); - if(getArguments().containsKey("editStatus")){ + if(getArguments().containsKey("editStatus")) editingStatus=Parcels.unwrap(getArguments().getParcelable("editStatus")); - } + if(getArguments().containsKey("replyTo")) + replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo")); if(instance==null){ Nav.finish(this); return; @@ -302,6 +305,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr selfName=view.findViewById(R.id.self_name); selfUsername=view.findViewById(R.id.self_username); selfAvatar=view.findViewById(R.id.self_avatar); + selfExtraText=view.findViewById(R.id.self_extra_text); HtmlParser.setTextWithCustomEmoji(selfName, self.displayName, self.emojis); selfUsername.setText('@'+self.username+'@'+instanceDomain); ViewImageLoader.load(selfAvatar, null, new UrlImageLoaderRequest(self.avatar)); @@ -343,6 +347,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr pollBtn.setOnClickListener(v->togglePoll()); emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText)); spoilerBtn.setOnClickListener(v->toggleSpoiler()); + + localOnly = savedInstanceState != null ? savedInstanceState.getBoolean("localOnly") : + editingStatus != null ? editingStatus.localOnly : replyTo != null && replyTo.localOnly; + buildVisibilityPopup(visibilityBtn); visibilityBtn.setOnClickListener(v->visibilityPopup.show()); visibilityBtn.setOnTouchListener(visibilityPopup.getDragToOpenListener()); @@ -462,6 +470,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr case PRIVATE -> R.id.vis_followers; case DIRECT -> R.id.vis_private; }).setChecked(true); + visibilityPopup.getMenu().findItem(R.id.local_only).setChecked(localOnly); autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID); autocompleteViewController.setCompletionSelectedListener(this::onAutocompleteOptionSelected); @@ -488,6 +497,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr outState.putBoolean("pollAllowMultiple", pollAllowMultipleItem.isSelected()); } outState.putBoolean("sensitive", sensitive); + outState.putBoolean("localOnly", localOnly); outState.putBoolean("hasSpoiler", hasSpoiler); outState.putString("language", language); if(!attachments.isEmpty()){ @@ -611,6 +621,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } }); View originalPost = view.findViewById(R.id.original_post); + extraText = view.findViewById(R.id.extra_text); originalPost.setVisibility(View.VISIBLE); originalPost.setOnClickListener(v->{ Bundle args=new Bundle(); @@ -749,6 +760,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } updateSensitive(); + updateHeaders(); if(editingStatus!=null){ updateCharCounter(); @@ -976,7 +988,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private void publish(boolean force){ String text=mainEditText.getText().toString(); CreateStatus.Request req=new CreateStatus.Request(); + if (GlobalUserPreferences.accountsInGlitchMode.contains(accountID)) { + text += " " + GLITCH_LOCAL_ONLY_SUFFIX; + } req.status=text; + req.localOnly=localOnly; req.visibility=statusVisibility; req.sensitive=sensitive; req.language=language; @@ -1758,12 +1774,32 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr return attachments.size(); } + private void updateHeaders() { + UiUtils.setExtraTextInfo(getContext(), selfExtraText, statusVisibility, localOnly); + if (replyTo != null) UiUtils.setExtraTextInfo(getContext(), extraText, replyTo.visibility, replyTo.localOnly); + } + private void buildVisibilityPopup(View v){ visibilityPopup=new PopupMenu(getActivity(), v); visibilityPopup.inflate(R.menu.compose_visibility); Menu m=visibilityPopup.getMenu(); + MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only); + boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID); + if (localOnly || prefsSaysSupported) { + localOnlyItem.setChecked(localOnly); + Status status = editingStatus != null ? editingStatus : replyTo; + if (!prefsSaysSupported) { + GlobalUserPreferences.accountsWithLocalOnlySupport.add(accountID); + if (status.getStrippedText().matches("[\\s\\S]*" + GLITCH_LOCAL_ONLY_SUFFIX + "[\uFE00-\uFE0F]*")) { + GlobalUserPreferences.accountsInGlitchMode.add(accountID); + } + GlobalUserPreferences.save(); + } + } else { + localOnlyItem.setVisible(false); + } UiUtils.enablePopupMenuIcons(getActivity(), visibilityPopup); - m.setGroupCheckable(0, true, true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) m.setGroupDividerEnabled(true); visibilityPopup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){ @Override public boolean onMenuItemClick(MenuItem item){ @@ -1777,18 +1813,21 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr }else if(id==R.id.vis_private){ statusVisibility=StatusPrivacy.DIRECT; } - item.setChecked(true); + if (id == R.id.local_only) { + localOnly = !item.isChecked(); + item.setChecked(localOnly); + } else { + item.setChecked(true); + } updateVisibilityIcon(); + updateHeaders(); return true; } }); } private void loadDefaultStatusVisibility(Bundle savedInstanceState) { - if(getArguments().containsKey("replyTo")){ - replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo")); - statusVisibility = replyTo.visibility; - } + if(replyTo != null) statusVisibility = replyTo.visibility; // A saved privacy setting from a previous compose session wins over the reply visibility if(savedInstanceState !=null){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java index 9d4cdcd65..47d2d4be0 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java @@ -77,7 +77,7 @@ public class SettingsFragment extends MastodonToolbarFragment{ private ArrayList items=new ArrayList<>(); private ThemeItem themeItem; private NotificationPolicyItem notificationPolicyItem; - private SwitchItem loadNewPostsItem, showNewPostsButtonItem; + private SwitchItem showNewPostsButtonItem, glitchModeItem; private String accountID; private boolean needUpdateNotificationSettings; private boolean needAppRestart; @@ -225,7 +225,7 @@ public class SettingsFragment extends MastodonToolbarFragment{ GlobalUserPreferences.showBoosts=i.checked; GlobalUserPreferences.save(); })); - items.add(loadNewPostsItem = new SwitchItem(R.string.sk_settings_load_new_posts, R.drawable.ic_fluent_arrow_sync_24_regular, GlobalUserPreferences.loadNewPosts, i->{ + items.add(new SwitchItem(R.string.sk_settings_load_new_posts, R.drawable.ic_fluent_arrow_sync_24_regular, GlobalUserPreferences.loadNewPosts, i->{ GlobalUserPreferences.loadNewPosts=i.checked; showNewPostsButtonItem.enabled = i.checked; if (!i.checked) { @@ -239,7 +239,6 @@ public class SettingsFragment extends MastodonToolbarFragment{ GlobalUserPreferences.showNewPostsButton=i.checked; GlobalUserPreferences.save(); })); - showNewPostsButtonItem.enabled = GlobalUserPreferences.loadNewPosts; items.add(new HeaderItem(R.string.settings_notifications)); items.add(notificationPolicyItem=new NotificationPolicyItem()); @@ -267,6 +266,25 @@ public class SettingsFragment extends MastodonToolbarFragment{ items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular)); items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular)); items.add(new TextItem(R.string.log_out, this::confirmLogOut, R.drawable.ic_fluent_sign_out_24_regular)); + items.add(new SwitchItem(R.string.sk_settings_support_local_only, 0, GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID), i->{ + glitchModeItem.enabled = i.checked; + if (i.checked) { + GlobalUserPreferences.accountsWithLocalOnlySupport.add(accountID); + } else { + GlobalUserPreferences.accountsWithLocalOnlySupport.remove(accountID); + } + if (list.findViewHolderForAdapterPosition(items.indexOf(glitchModeItem)) instanceof SwitchViewHolder svh) svh.rebind(); + GlobalUserPreferences.save(); + })); + items.add(glitchModeItem = new SwitchItem(R.string.sk_settings_glitch_instance, 0, GlobalUserPreferences.accountsInGlitchMode.contains(accountID), i->{ + if (i.checked) { + GlobalUserPreferences.accountsInGlitchMode.add(accountID); + } else { + GlobalUserPreferences.accountsInGlitchMode.remove(accountID); + } + GlobalUserPreferences.save(); + })); + glitchModeItem.enabled = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID); items.add(new HeaderItem(R.string.sk_settings_about)); items.add(new TextItem(R.string.sk_settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/sk22/megalodon"), R.drawable.ic_fluent_open_24_regular)); 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 d8bb2e3b0..07612f657 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 @@ -124,6 +124,7 @@ public class SearchFragment extends BaseStatusListFragment impleme @Override protected void doLoadData(int offset, int count){ + if (getActivity() == null) return; resetEmptyText(); if(isInRecentMode()){ AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(sr->{ 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 fb03cbce8..b41f56e54 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 @@ -319,17 +319,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ } itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0); if(TextUtils.isEmpty(item.extraText)){ - List extraParts = new ArrayList<>(); - if (item.status != null && item.status.localOnly) - extraParts.add(item.parentFragment.getString(R.string.sk_inline_local_only)); - if (item.status != null && item.status.visibility.equals(StatusPrivacy.DIRECT)) - extraParts.add(item.parentFragment.getString(R.string.sk_inline_direct)); - if (!extraParts.isEmpty()) { - String sep = item.parentFragment.getString(R.string.sk_separator); - extraText.setText(String.join(" " + sep + " ", extraParts)); - extraText.setVisibility(View.VISIBLE); - } else { - extraText.setVisibility(View.GONE); + if (item.status != null) { + UiUtils.setExtraTextInfo(item.parentFragment.getContext(), extraText, item.status.visibility, item.status.localOnly); } }else{ extraText.setVisibility(View.VISIBLE); 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 ee216a4d5..870de552a 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 @@ -73,13 +73,11 @@ import org.joinmastodon.android.events.StatusDeletedEvent; import org.joinmastodon.android.events.StatusUnpinnedEvent; import org.joinmastodon.android.fragments.ComposeFragment; import org.joinmastodon.android.fragments.HashtagTimelineFragment; -import org.joinmastodon.android.fragments.ListTimelineFragment; import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ThreadFragment; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Instance; -import org.joinmastodon.android.model.ListTimeline; import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.ScheduledStatus; @@ -99,6 +97,7 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -912,6 +911,22 @@ public class UiUtils{ return back; } + public static boolean setExtraTextInfo(Context ctx, TextView extraText, StatusPrivacy visibility, boolean localOnly) { + List extraParts = new ArrayList<>(); + if (localOnly) extraParts.add(ctx.getString(R.string.sk_inline_local_only)); + if (visibility != null &&visibility.equals(StatusPrivacy.DIRECT)) + extraParts.add(ctx.getString(R.string.sk_inline_direct)); + if (!extraParts.isEmpty()) { + String sep = ctx.getString(R.string.sk_separator); + extraText.setText(String.join(" " + sep + " ", extraParts)); + extraText.setVisibility(View.VISIBLE); + return true; + } else { + extraText.setVisibility(View.GONE); + return false; + } + } + @FunctionalInterface public interface InteractionPerformer { void interact(StatusInteractionController ic, Status status, Consumer resultConsumer); diff --git a/mastodon/src/main/res/layout/fragment_compose.xml b/mastodon/src/main/res/layout/fragment_compose.xml index 67215fdd1..42c684975 100644 --- a/mastodon/src/main/res/layout/fragment_compose.xml +++ b/mastodon/src/main/res/layout/fragment_compose.xml @@ -87,7 +87,7 @@ + + - - - - + + + + + + + + + \ 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 6823d8ba3..2b13aa74a 100644 --- a/mastodon/src/main/res/values/strings_sk.xml +++ b/mastodon/src/main/res/values/strings_sk.xml @@ -239,4 +239,7 @@ local-only direct ยท + Instance supports local-only posting + Use Glitch implementation + Local-only \ No newline at end of file