newRecentLanguages = new ArrayList<>(Optional.ofNullable(recentLanguages.get(accountID)).orElse(defaultRecentLanguages));
newRecentLanguages.remove(language);
newRecentLanguages.add(0, language);
+ if (encoding != null) {
+ newRecentLanguages.remove(encoding);
+ newRecentLanguages.add(0, encoding);
+ }
+ if ("bottom".equals(encoding) && !GlobalUserPreferences.bottomEncoding) {
+ GlobalUserPreferences.bottomEncoding = true;
+ GlobalUserPreferences.save();
+ }
recentLanguages.put(accountID, newRecentLanguages.stream().limit(4).collect(Collectors.toList()));
GlobalUserPreferences.save();
}
@@ -1129,7 +1248,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
private void confirmDiscardDraftAndFinish(){
- new M3AlertDialogBuilder(getActivity())
+ boolean attachmentsPending = attachments.stream().anyMatch(att -> att.state != AttachmentUploadState.DONE);
+ if (attachmentsPending) new M3AlertDialogBuilder(getActivity())
+ .setTitle(R.string.sk_unfinished_attachments)
+ .setMessage(R.string.sk_unfinished_attachments_message)
+ .setPositiveButton(R.string.edit, (d, w) -> {})
+ .setNegativeButton(R.string.discard, (d, w) -> Nav.finish(this))
+ .show();
+ else new M3AlertDialogBuilder(getActivity())
.setTitle(editingStatus != null ? R.string.sk_confirm_save_changes : R.string.sk_confirm_save_draft)
.setPositiveButton(R.string.save, (d, w) -> {
updateScheduledAt(scheduledAt == null ? getDraftInstant() : scheduledAt);
@@ -1139,18 +1265,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
.show();
}
- /**
- * Check to see if Android platform photopicker is available on the device\
- * @return whether the device supports photopicker intents.
- */
- private boolean isPhotoPickerAvailable() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- return true;
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- return getExtensionVersion(Build.VERSION_CODES.R) >= 2;
- } else
- return false;
- }
/**
* Builds the correct intent for the device version to select media.
@@ -1160,26 +1274,26 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
*
* For earlier versions use the built in docs ui via {@link Intent#ACTION_GET_CONTENT}
*/
- private void openFilePicker(){
+ private void openFilePicker(boolean photoPicker){
Intent intent;
- boolean usePhotoPicker = isPhotoPickerAvailable();
- if (usePhotoPicker) {
- intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
- intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
- } else {
- intent = new Intent(Intent.ACTION_GET_CONTENT);
+ boolean usePhotoPicker=photoPicker && isPhotoPickerAvailable();
+ if(usePhotoPicker){
+ intent=new Intent(MediaStore.ACTION_PICK_IMAGES);
+ intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MAX_ATTACHMENTS-getMediaAttachmentsCount());
+ }else{
+ intent=new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
}
- if (!usePhotoPicker && instance.configuration != null &&
- instance.configuration.mediaAttachments != null &&
- instance.configuration.mediaAttachments.supportedMimeTypes != null &&
- !instance.configuration.mediaAttachments.supportedMimeTypes.isEmpty()) {
+ if(!usePhotoPicker && instance.configuration!=null &&
+ instance.configuration.mediaAttachments!=null &&
+ instance.configuration.mediaAttachments.supportedMimeTypes!=null &&
+ !instance.configuration.mediaAttachments.supportedMimeTypes.isEmpty()){
intent.putExtra(Intent.EXTRA_MIME_TYPES,
instance.configuration.mediaAttachments.supportedMimeTypes.toArray(
new String[0]));
- } else {
- if (!usePhotoPicker) {
+ }else{
+ if(!usePhotoPicker){
// If photo picker is being used these are the default mimetypes.
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
}
@@ -1524,6 +1638,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
if(att.serverAttachment==null)
return;
+ editMediaDescription(att);
+ }
+
+ private void editMediaDescription(DraftMediaAttachment att) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("attachment", att.serverAttachment.id);
@@ -1600,18 +1718,20 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
menu.getMenu().add(0, 2, 0, getResources().getQuantityString(R.plurals.x_minutes, 30, 30));
menu.getMenu().add(0, 3, 0, getResources().getQuantityString(R.plurals.x_hours, 1, 1));
menu.getMenu().add(0, 4, 0, getResources().getQuantityString(R.plurals.x_hours, 6, 6));
- menu.getMenu().add(0, 5, 0, getResources().getQuantityString(R.plurals.x_days, 1, 1));
- menu.getMenu().add(0, 6, 0, getResources().getQuantityString(R.plurals.x_days, 3, 3));
- menu.getMenu().add(0, 7, 0, getResources().getQuantityString(R.plurals.x_days, 7, 7));
+ menu.getMenu().add(0, 5, 0, getResources().getQuantityString(R.plurals.x_hours, 12, 12));
+ menu.getMenu().add(0, 6, 0, getResources().getQuantityString(R.plurals.x_days, 1, 1));
+ menu.getMenu().add(0, 7, 0, getResources().getQuantityString(R.plurals.x_days, 3, 3));
+ menu.getMenu().add(0, 8, 0, getResources().getQuantityString(R.plurals.x_days, 7, 7));
menu.setOnMenuItemClickListener(item->{
pollDuration=switch(item.getItemId()){
case 1 -> 5*60;
case 2 -> 30*60;
case 3 -> 3600;
case 4 -> 6*3600;
- case 5 -> 24*3600;
- case 6 -> 3*24*3600;
- case 7 -> 7*24*3600;
+ case 5 -> 12*3600;
+ case 6 -> 24*3600;
+ case 7 -> 3*24*3600;
+ case 8 -> 7*24*3600;
default -> throw new IllegalStateException("Unexpected value: "+item.getItemId());
};
pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr=item.getTitle().toString()));
@@ -1705,12 +1825,33 @@ 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 (instance.pleroma != null) {
+ m.findItem(R.id.vis_local).setVisible(true);
+ } else if (localOnly || prefsSaysSupported) {
+ localOnlyItem.setVisible(true);
+ localOnlyItem.setChecked(localOnly);
+ Status status = editingStatus != null ? editingStatus : replyTo;
+ if (!prefsSaysSupported) {
+ GlobalUserPreferences.accountsWithLocalOnlySupport.add(accountID);
+ if (GLITCH_LOCAL_ONLY_PATTERN.matcher(status.getStrippedText()).matches()) {
+ GlobalUserPreferences.accountsInGlitchMode.add(accountID);
+ }
+ GlobalUserPreferences.save();
+ }
+ }
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){
@@ -1723,41 +1864,44 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
statusVisibility=StatusPrivacy.PRIVATE;
}else if(id==R.id.vis_private){
statusVisibility=StatusPrivacy.DIRECT;
+ }else if(id==R.id.vis_local){
+ statusVisibility=StatusPrivacy.LOCAL;
+ }
+ if (id == R.id.local_only) {
+ localOnly = !item.isChecked();
+ item.setChecked(localOnly);
+ } else {
+ item.setChecked(true);
}
- 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){
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
}
- Preferences prefs = AccountSessionManager.getInstance().getAccount(accountID).preferences;
+ AccountSessionManager asm = AccountSessionManager.getInstance();
+ Preferences prefs = asm.getAccount(accountID).preferences;
if (prefs != null) {
// Only override the reply visibility if our preference is more private
- if (prefs.postingDefaultVisibility.isLessVisibleThan(statusVisibility)) {
- statusVisibility = switch (prefs.postingDefaultVisibility) {
- case PUBLIC -> StatusPrivacy.PUBLIC;
- case UNLISTED -> StatusPrivacy.UNLISTED;
- case PRIVATE -> StatusPrivacy.PRIVATE;
- case DIRECT -> StatusPrivacy.DIRECT;
- };
+ // (and we're not replying to ourselves, or not at all)
+ if (prefs.postingDefaultVisibility.isLessVisibleThan(statusVisibility) &&
+ (replyTo == null || !asm.isSelf(accountID, replyTo.account))) {
+ statusVisibility = prefs.postingDefaultVisibility;
}
+ }
- // A saved privacy setting from a previous compose session wins over all
- if(savedInstanceState !=null){
- statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
- }
+ // A saved privacy setting from a previous compose session wins over all
+ if(savedInstanceState !=null){
+ statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility");
}
}
@@ -1767,9 +1911,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
visibilityBtn.setImageResource(switch(statusVisibility){
case PUBLIC -> R.drawable.ic_fluent_earth_24_regular;
- case UNLISTED -> R.drawable.ic_fluent_people_community_24_regular;
- case PRIVATE -> R.drawable.ic_fluent_people_checkmark_24_regular;
+ case UNLISTED -> R.drawable.ic_fluent_lock_open_24_regular;
+ case PRIVATE -> R.drawable.ic_fluent_lock_closed_24_filled;
case DIRECT -> R.drawable.ic_fluent_mention_24_regular;
+ case LOCAL -> R.drawable.ic_fluent_eye_24_regular;
});
}
diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/EditTimelinesFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/EditTimelinesFragment.java
new file mode 100644
index 000000000..c947b0fa9
--- /dev/null
+++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/EditTimelinesFragment.java
@@ -0,0 +1,352 @@
+package org.joinmastodon.android.fragments;
+
+import static android.view.Menu.NONE;
+
+import static org.joinmastodon.android.ui.utils.UiUtils.makeBackItem;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Bundle;
+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.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.ItemTouchHelper;
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.joinmastodon.android.GlobalUserPreferences;
+import org.joinmastodon.android.R;
+import org.joinmastodon.android.api.requests.lists.GetLists;
+import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
+import org.joinmastodon.android.model.Hashtag;
+import org.joinmastodon.android.model.HeaderPaginationList;
+import org.joinmastodon.android.model.ListTimeline;
+import org.joinmastodon.android.model.TimelineDefinition;
+import org.joinmastodon.android.ui.DividerItemDecoration;
+import org.joinmastodon.android.ui.M3AlertDialogBuilder;
+import org.joinmastodon.android.ui.utils.UiUtils;
+import org.joinmastodon.android.ui.views.TextInputFrameLayout;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import me.grishka.appkit.api.Callback;
+import me.grishka.appkit.api.ErrorResponse;
+import me.grishka.appkit.fragments.BaseRecyclerFragment;
+import me.grishka.appkit.utils.BindableViewHolder;
+import me.grishka.appkit.views.UsableRecyclerView;
+
+public class EditTimelinesFragment extends BaseRecyclerFragment implements ScrollableToTop {
+ private String accountID;
+ private TimelinesAdapter adapter;
+ private final ItemTouchHelper itemTouchHelper;
+ private Menu optionsMenu;
+ private boolean updated;
+ private final Map