Start adding "approve" mod action.

This commit is contained in:
Docile-Alligator 2025-07-09 18:09:44 -04:00
parent a698f32fd6
commit f517ea91ea
15 changed files with 308 additions and 12 deletions

View File

@ -0,0 +1,7 @@
package ml.docilealligator.infinityforreddit
import ml.docilealligator.infinityforreddit.post.Post
interface PostModerationActionHandler {
fun approvePost(post: Post?)
}

View File

@ -0,0 +1,51 @@
package ml.docilealligator.infinityforreddit;
import android.util.Log;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import java.util.concurrent.atomic.AtomicBoolean;
public class SingleLiveEvent<T> extends MutableLiveData<T> {
private static final String TAG = "SingleLiveEvent";
private final AtomicBoolean mPending = new AtomicBoolean(false);
@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull final Observer<? super T> observer) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.");
}
// Observe the internal MutableLiveData
super.observe(owner, new Observer<T>() {
@Override
public void onChanged(@Nullable T t) {
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t);
}
}
});
}
@MainThread
public void setValue(@Nullable T t) {
mPending.set(true);
super.setValue(t);
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
public void call() {
setValue(null);
}
}

View File

@ -351,7 +351,7 @@ public class PostDetailRecyclerViewAdapter extends RecyclerView.Adapter<Recycler
textView.setOnLongClickListener(view -> {
if (textView.getSelectionStart() == -1 && textView.getSelectionEnd() == -1) {
CopyTextBottomSheetFragment.show(
mActivity.getSupportFragmentManager(),
mFragment.getChildFragmentManager(),
mPost.getSelfTextPlain(), mPost.getSelfText()
);
return true;
@ -382,7 +382,7 @@ public class PostDetailRecyclerViewAdapter extends RecyclerView.Adapter<Recycler
Bundle bundle = new Bundle();
bundle.putString(UrlMenuBottomSheetFragment.EXTRA_URL, url);
urlMenuBottomSheetFragment.setArguments(bundle);
urlMenuBottomSheetFragment.show(activity.getSupportFragmentManager(), urlMenuBottomSheetFragment.getTag());
urlMenuBottomSheetFragment.show(fragment.getChildFragmentManager(), urlMenuBottomSheetFragment.getTag());
}
return true;
};
@ -1267,7 +1267,7 @@ public class PostDetailRecyclerViewAdapter extends RecyclerView.Adapter<Recycler
} else {
postOptionsBottomSheetFragment = PostOptionsBottomSheetFragment.newInstance(mPost, mFragment.getPostListPosition());
}
postOptionsBottomSheetFragment.show(mActivity.getSupportFragmentManager(), postOptionsBottomSheetFragment.getTag());
postOptionsBottomSheetFragment.show(mFragment.getChildFragmentManager(), postOptionsBottomSheetFragment.getTag());
return true;
});
@ -1344,7 +1344,7 @@ public class PostDetailRecyclerViewAdapter extends RecyclerView.Adapter<Recycler
mMarkwonAdapter.setOnLongClickListener(v -> {
CopyTextBottomSheetFragment.show(
mActivity.getSupportFragmentManager(),
mFragment.getChildFragmentManager(),
mPost.getSelfTextPlain(), mPost.getSelfText()
);
return true;
@ -1630,7 +1630,7 @@ public class PostDetailRecyclerViewAdapter extends RecyclerView.Adapter<Recycler
bundle.putParcelable(ShareBottomSheetFragment.EXTRA_POST, mPost);
ShareBottomSheetFragment shareBottomSheetFragment = new ShareBottomSheetFragment();
shareBottomSheetFragment.setArguments(bundle);
shareBottomSheetFragment.show(mActivity.getSupportFragmentManager(), shareBottomSheetFragment.getTag());
shareBottomSheetFragment.show(mFragment.getChildFragmentManager(), shareBottomSheetFragment.getTag());
});
this.shareButton.setOnLongClickListener(view -> {

View File

@ -2994,7 +2994,7 @@ public class PostRecyclerViewAdapter extends PagingDataAdapter<Post, RecyclerVie
} else {
postOptionsBottomSheetFragment = PostOptionsBottomSheetFragment.newInstance(post, getBindingAdapterPosition());
}
postOptionsBottomSheetFragment.show(mActivity.getSupportFragmentManager(), postOptionsBottomSheetFragment.getTag());
postOptionsBottomSheetFragment.show(mFragment.getChildFragmentManager(), postOptionsBottomSheetFragment.getTag());
}
@Override
@ -4017,7 +4017,7 @@ public class PostRecyclerViewAdapter extends PagingDataAdapter<Post, RecyclerVie
PostOptionsBottomSheetFragment postOptionsBottomSheetFragment;
postOptionsBottomSheetFragment = PostOptionsBottomSheetFragment.newInstance(post, getBindingAdapterPosition());
postOptionsBottomSheetFragment.show(mActivity.getSupportFragmentManager(), postOptionsBottomSheetFragment.getTag());
postOptionsBottomSheetFragment.show(mFragment.getChildFragmentManager(), postOptionsBottomSheetFragment.getTag());
}
@Override

View File

@ -429,4 +429,8 @@ public interface RedditAPI {
@GET("/api/user_data_by_account_ids.json")
Call<String> loadPartialUserData(@Query("ids") String commaSeparatedUserFullNames);
@FormUrlEncoded
@POST("/api/approve")
Call<String> approveThing(@HeaderMap Map<String, String> headers, @FieldMap Map<String, String> params);
}

View File

@ -0,0 +1,57 @@
package ml.docilealligator.infinityforreddit.bottomsheetfragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import ml.docilealligator.infinityforreddit.PostModerationActionHandler
import ml.docilealligator.infinityforreddit.R
import ml.docilealligator.infinityforreddit.customviews.LandscapeExpandedRoundedBottomSheetDialogFragment
import ml.docilealligator.infinityforreddit.databinding.FragmentModerationActionBottomSheetBinding
import ml.docilealligator.infinityforreddit.post.Post
private const val EXTRA_POST = "EP"
/**
* A simple [Fragment] subclass.
* Use the [ModerationActionBottomSheetFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class ModerationActionBottomSheetFragment : LandscapeExpandedRoundedBottomSheetDialogFragment() {
private var post: Post? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
post = it.getParcelable(EXTRA_POST)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
val binding: FragmentModerationActionBottomSheetBinding = FragmentModerationActionBottomSheetBinding.inflate(inflater, container, false)
if (parentFragment is PostModerationActionHandler) {
binding.approveTextViewModerationActionBottomSheetFragment.setOnClickListener {
(parentFragment as PostModerationActionHandler).approvePost(post)
dismiss()
}
}
return binding.root
}
companion object {
@JvmStatic
fun newInstance(post: Post) =
ModerationActionBottomSheetFragment().apply {
arguments = Bundle().apply {
putParcelable(EXTRA_POST, post)
}
}
}
}

View File

@ -176,7 +176,12 @@ public class PostOptionsBottomSheetFragment extends LandscapeExpandedRoundedBott
bundle.putParcelable(ShareBottomSheetFragment.EXTRA_POST, mPost);
ShareBottomSheetFragment shareBottomSheetFragment = new ShareBottomSheetFragment();
shareBottomSheetFragment.setArguments(bundle);
shareBottomSheetFragment.show(mBaseActivity.getSupportFragmentManager(), shareBottomSheetFragment.getTag());
Fragment parentFragment = getParentFragment();
if (parentFragment != null) {
shareBottomSheetFragment.show(parentFragment.getChildFragmentManager(), shareBottomSheetFragment.getTag());
} else {
shareBottomSheetFragment.show(mBaseActivity.getSupportFragmentManager(), shareBottomSheetFragment.getTag());
}
dismiss();
});
@ -275,6 +280,13 @@ public class PostOptionsBottomSheetFragment extends LandscapeExpandedRoundedBott
if (mPost.isCanModPost()) {
binding.modTextViewPostOptionsBottomSheetFragment.setVisibility(View.VISIBLE);
binding.modTextViewPostOptionsBottomSheetFragment.setOnClickListener(view -> {
ModerationActionBottomSheetFragment moderationActionBottomSheetFragment = ModerationActionBottomSheetFragment.newInstance(mPost);
Fragment parentFragment = getParentFragment();
if (parentFragment != null) {
moderationActionBottomSheetFragment.show(parentFragment.getChildFragmentManager(), moderationActionBottomSheetFragment.getTag());
} else {
moderationActionBottomSheetFragment.show(mBaseActivity.getSupportFragmentManager(), moderationActionBottomSheetFragment.getTag());
}
dismiss();
});
}

View File

@ -14,9 +14,11 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.paging.LoadState;
import androidx.recyclerview.widget.RecyclerView;
@ -38,6 +40,7 @@ import javax.inject.Provider;
import ml.docilealligator.infinityforreddit.FetchPostFilterAndConcatenatedSubredditNames;
import ml.docilealligator.infinityforreddit.Infinity;
import ml.docilealligator.infinityforreddit.PostModerationActionHandler;
import ml.docilealligator.infinityforreddit.R;
import ml.docilealligator.infinityforreddit.RecyclerViewContentScrollingInterface;
import ml.docilealligator.infinityforreddit.account.Account;
@ -58,6 +61,7 @@ import ml.docilealligator.infinityforreddit.events.ChangeNetworkStatusEvent;
import ml.docilealligator.infinityforreddit.events.ChangeSavePostFeedScrolledPositionEvent;
import ml.docilealligator.infinityforreddit.events.NeedForPostListFromPostFragmentEvent;
import ml.docilealligator.infinityforreddit.events.ProvidePostListToViewPostDetailActivityEvent;
import ml.docilealligator.infinityforreddit.moderation.ModerationEvent;
import ml.docilealligator.infinityforreddit.post.Post;
import ml.docilealligator.infinityforreddit.post.PostPagingSource;
import ml.docilealligator.infinityforreddit.post.PostViewModel;
@ -79,7 +83,7 @@ import retrofit2.Retrofit;
/**
* A simple {@link PostFragmentBase} subclass.
*/
public class PostFragment extends PostFragmentBase implements FragmentCommunicator {
public class PostFragment extends PostFragmentBase implements FragmentCommunicator, PostModerationActionHandler {
public static final String EXTRA_NAME = "EN";
public static final String EXTRA_USER_NAME = "EUN";
@ -877,6 +881,13 @@ public class PostFragment extends PostFragmentBase implements FragmentCommunicat
private void bindPostViewModel() {
mPostViewModel.getPosts().observe(getViewLifecycleOwner(), posts -> mAdapter.submitData(getViewLifecycleOwner().getLifecycle(), posts));
mPostViewModel.moderationEventLiveData.observe(getViewLifecycleOwner(), new Observer<>() {
@Override
public void onChanged(ModerationEvent moderationEvent) {
Toast.makeText(activity, R.string.approved, Toast.LENGTH_SHORT).show();
}
});
mAdapter.addLoadStateListener(combinedLoadStates -> {
LoadState refreshLoadState = combinedLoadStates.getRefresh();
LoadState appendLoadState = combinedLoadStates.getAppend();
@ -1382,4 +1393,9 @@ public class PostFragment extends PostFragmentBase implements FragmentCommunicat
mAdapter.setCanPlayVideo(hasWindowsFocus);
}
}
@Override
public void approvePost(@Nullable Post post) {
mPostViewModel.approvePost(post);
}
}

View File

@ -0,0 +1,5 @@
package ml.docilealligator.infinityforreddit.moderation
enum class ModerationEvent {
APPROVED, APPROVE_FAILED, REMOVED, REMOVE_FAILED, MARKED_AS_SPAM, MARK_AS_SPAM_FAILED
}

View File

@ -18,13 +18,22 @@ import androidx.paging.PagingData;
import androidx.paging.PagingDataTransforms;
import androidx.paging.PagingLiveData;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import ml.docilealligator.infinityforreddit.SingleLiveEvent;
import ml.docilealligator.infinityforreddit.account.Account;
import ml.docilealligator.infinityforreddit.apis.RedditAPI;
import ml.docilealligator.infinityforreddit.moderation.ModerationEvent;
import ml.docilealligator.infinityforreddit.postfilter.PostFilter;
import ml.docilealligator.infinityforreddit.readpost.ReadPostsListInterface;
import ml.docilealligator.infinityforreddit.thing.SortType;
import ml.docilealligator.infinityforreddit.account.Account;
import ml.docilealligator.infinityforreddit.postfilter.PostFilter;
import ml.docilealligator.infinityforreddit.utils.APIUtils;
import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
public class PostViewModel extends ViewModel {
@ -51,6 +60,8 @@ public class PostViewModel extends ViewModel {
private final MutableLiveData<PostFilter> postFilterLiveData;
private final SortTypeAndPostFilterLiveData sortTypeAndPostFilterLiveData;
public final SingleLiveEvent<ModerationEvent> moderationEventLiveData = new SingleLiveEvent<>();
// PostPagingSource.TYPE_FRONT_PAGE
public PostViewModel(Executor executor, Retrofit retrofit, @Nullable String accessToken, @NonNull String accountName,
SharedPreferences sharedPreferences, SharedPreferences postFeedScrolledPositionSharedPreferences,
@ -482,4 +493,29 @@ public class PostViewModel extends ViewModel {
addSource(postFilterLiveData, postFilter -> setValue(Pair.create(postFilter, sortTypeLiveData.getValue())));
}
}
public void approvePost(@Nullable Post post) {
if (post == null) {
moderationEventLiveData.postValue(ModerationEvent.APPROVE_FAILED);
return;
}
Map<String, String> params = new HashMap<>();
params.put(APIUtils.ID_KEY, post.getFullName());
retrofit.create(RedditAPI.class).approveThing(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback<>() {
@Override
public void onResponse(@NonNull Call<String> call, @NonNull Response<String> response) {
if (response.isSuccessful()) {
moderationEventLiveData.postValue(ModerationEvent.APPROVED);
} else {
moderationEventLiveData.postValue(ModerationEvent.APPROVE_FAILED);
}
}
@Override
public void onFailure(@NonNull Call<String> call, @NonNull Throwable throwable) {
moderationEventLiveData.postValue(ModerationEvent.APPROVE_FAILED);
}
});
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M70,522q-12,-12 -11.5,-28T71,466q12,-11 28,-11.5t28,11.5l142,142 14,14 14,14q12,12 11.5,28T296,692q-12,11 -28,11.5T240,692L70,522ZM494,607 L834,267q12,-12 28,-11.5t28,12.5q11,12 11.5,28T890,324L522,692q-12,12 -28,12t-28,-12L296,522q-11,-11 -11,-27.5t11,-28.5q12,-12 28.5,-12t28.5,12l141,141ZM663,325L522,466q-11,11 -27.5,11T466,466q-12,-12 -12,-28.5t12,-28.5l141,-141q11,-11 27.5,-11t28.5,11q12,12 12,28.5T663,325Z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M480,536 L284,732q-11,11 -28,11t-28,-11q-11,-11 -11,-28t11,-28l196,-196 -196,-196q-11,-11 -11,-28t11,-28q11,-11 28,-11t28,11l196,196 196,-196q11,-11 28,-11t28,11q11,11 11,28t-11,28L536,480l196,196q11,11 11,28t-11,28q-11,11 -28,11t-28,-11L480,536Z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M280,840q-33,0 -56.5,-23.5T200,760v-520q-17,0 -28.5,-11.5T160,200q0,-17 11.5,-28.5T200,160h160q0,-17 11.5,-28.5T400,120h160q17,0 28.5,11.5T600,160h160q17,0 28.5,11.5T800,200q0,17 -11.5,28.5T760,240v520q0,33 -23.5,56.5T680,840L280,840ZM680,240L280,240v520h400v-520ZM400,680q17,0 28.5,-11.5T440,640v-280q0,-17 -11.5,-28.5T400,320q-17,0 -28.5,11.5T360,360v280q0,17 11.5,28.5T400,680ZM560,680q17,0 28.5,-11.5T600,640v-280q0,-17 -11.5,-28.5T560,320q-17,0 -28.5,11.5T520,360v280q0,17 11.5,28.5T560,680ZM280,240v520,-520Z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never"
android:paddingBottom="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/approve_text_view_moderation_action_bottom_sheet_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:drawableStart="@drawable/ic_approve_24dp"
android:drawablePadding="48dp"
android:focusable="true"
android:fontFamily="?attr/font_family"
android:paddingStart="32dp"
android:paddingTop="16dp"
android:paddingEnd="32dp"
android:paddingBottom="16dp"
android:text="@string/approve"
android:textColor="?attr/primaryTextColor"
android:textSize="?attr/font_default"
app:drawableTint="?attr/primaryTextColor" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/remove_text_view_moderation_action_bottom_sheet_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:drawableStart="@drawable/ic_remove_24dp"
android:drawablePadding="48dp"
android:focusable="true"
android:fontFamily="?attr/font_family"
android:paddingStart="32dp"
android:paddingTop="16dp"
android:paddingEnd="32dp"
android:paddingBottom="16dp"
android:text="@string/remove"
android:textColor="?attr/primaryTextColor"
android:textSize="?attr/font_default"
app:drawableTint="?attr/primaryTextColor" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/spam_text_view_moderation_action_bottom_sheet_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:drawableStart="@drawable/ic_spam_24dp"
android:drawablePadding="48dp"
android:focusable="true"
android:fontFamily="?attr/font_family"
android:paddingStart="32dp"
android:paddingTop="16dp"
android:paddingEnd="32dp"
android:paddingBottom="16dp"
android:text="@string/mark_as_spam"
android:textColor="?attr/primaryTextColor"
android:textSize="?attr/font_default"
app:drawableTint="?attr/primaryTextColor" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -1540,7 +1540,7 @@
<string name="proxy_http">HTTP</string>
<string name="proxy_socks">SOCKS</string>
<string name="proxy_direct">Direct</string>
<string name="jump_to_parent_comment">Jump to Parent Comment</string>
<string name="long_press_post_non_media_area">Long Press on Post (Non-Media Area)</string>
<string name="long_press_post_media">Long Press on Media</string>
@ -1548,4 +1548,9 @@
<string name="preview_in_fullscreen">Preview in Fullscreen</string>
<string name="moderation">Moderation</string>
<string name="approve">Approve</string>
<string name="approved">Approved</string>
<string name="approve_failed">Approve failed</string>
<string name="remove">Remove</string>
<string name="mark_as_spam">Mark as spam</string>
</resources>