implement local-only posting

This commit is contained in:
sk 2023-01-24 16:04:17 +01:00
parent cf61626901
commit 2358d3c602
10 changed files with 136 additions and 38 deletions

View File

@ -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<Map<String, List<TimelineDefinition>>>() {}.getType();
public static Map<String, List<String>> recentLanguages;
public static Map<String, List<TimelineDefinition>> pinnedTimelines;
public static Set<String> accountsWithLocalOnlySupport;
public static Set<String> 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();
}

View File

@ -39,6 +39,7 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
public Poll poll;
public String inReplyToId;
public boolean sensitive;
public boolean localOnly;
public String spoilerText;
public StatusPrivacy visibility;
public Instant scheduledAt;

View File

@ -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){

View File

@ -77,7 +77,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
private ArrayList<Item> 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));

View File

@ -124,6 +124,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
@Override
protected void doLoadData(int offset, int count){
if (getActivity() == null) return;
resetEmptyText();
if(isInRecentMode()){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(sr->{

View File

@ -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<String> 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);

View File

@ -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<String> 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<Status> resultConsumer);

View File

@ -87,7 +87,7 @@
<TextView
android:id="@+id/self_name"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/self_avatar"
android:layout_marginTop="2sp"
@ -97,6 +97,20 @@
android:textAppearance="@style/m3_title_medium"
tools:text="Eugen" />
<TextView
android:id="@+id/self_extra_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2sp"
android:layout_marginStart="8sp"
android:layout_toEndOf="@id/self_name"
android:ellipsize="end"
android:fontFamily="sans-serif"
android:singleLine="true"
android:textAlignment="viewStart"
android:textAppearance="@style/m3_title_medium"
tools:text="@string/sk_local_only" />
<TextView
android:id="@+id/self_username"
android:layout_width="match_parent"

View File

@ -1,15 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/vis_public"
android:icon="@drawable/ic_fluent_earth_24_regular"
android:title="@string/visibility_public"/>
<item android:id="@+id/vis_unlisted"
android:icon="@drawable/ic_fluent_people_community_24_regular"
android:title="@string/sk_visibility_unlisted"/>
<item android:id="@+id/vis_followers"
android:icon="@drawable/ic_fluent_people_checkmark_24_regular"
android:title="@string/visibility_followers_only"/>
<item android:id="@+id/vis_private"
android:icon="@drawable/ic_fluent_mention_24_regular"
android:title="@string/visibility_private"/>
<group android:id="@+id/local_only_group" android:checkableBehavior="all">
<item
android:id="@+id/local_only"
android:icon="@drawable/ic_fluent_eye_24_regular"
android:title="@string/sk_local_only" />
</group>
<group android:id="@+id/visibility_group" android:checkableBehavior="single">
<item android:id="@+id/vis_public"
android:icon="@drawable/ic_fluent_earth_24_regular"
android:title="@string/visibility_public"/>
<item android:id="@+id/vis_unlisted"
android:icon="@drawable/ic_fluent_people_community_24_regular"
android:title="@string/sk_visibility_unlisted"/>
<item android:id="@+id/vis_followers"
android:icon="@drawable/ic_fluent_people_checkmark_24_regular"
android:title="@string/visibility_followers_only"/>
<item android:id="@+id/vis_private"
android:icon="@drawable/ic_fluent_mention_24_regular"
android:title="@string/visibility_private"/>
</group>
</menu>

View File

@ -239,4 +239,7 @@
<string name="sk_inline_local_only">local-only</string>
<string name="sk_inline_direct">direct</string>
<string name="sk_separator">·</string>
<string name="sk_settings_support_local_only">Instance supports local-only posting</string>
<string name="sk_settings_glitch_instance">Use Glitch implementation</string>
<string name="sk_local_only">Local-only</string>
</resources>