New mod options: Sticky post and lock post.

This commit is contained in:
Docile-Alligator 2025-07-09 23:21:40 -04:00
parent f517ea91ea
commit bdc7ce142a
14 changed files with 270 additions and 31 deletions

View File

@ -3,5 +3,8 @@ package ml.docilealligator.infinityforreddit
import ml.docilealligator.infinityforreddit.post.Post
interface PostModerationActionHandler {
fun approvePost(post: Post?)
fun approvePost(post: Post, position: Int)
fun removePost(post: Post, position: Int, isSpam: Boolean)
fun toggleSticky(post: Post, position: Int)
fun toggleLock(post: Post, position: Int)
}

View File

@ -433,4 +433,20 @@ public interface RedditAPI {
@FormUrlEncoded
@POST("/api/approve")
Call<String> approveThing(@HeaderMap Map<String, String> headers, @FieldMap Map<String, String> params);
@FormUrlEncoded
@POST("/api/remove")
Call<String> removeThing(@HeaderMap Map<String, String> headers, @FieldMap Map<String, String> params);
@FormUrlEncoded
@POST("/api/set_subreddit_sticky")
Call<String> toggleStickyPost(@HeaderMap Map<String, String> headers, @FieldMap Map<String, String> params);
@FormUrlEncoded
@POST("/api/lock")
Call<String> lockThing(@HeaderMap Map<String, String> headers, @FieldMap Map<String, String> params);
@FormUrlEncoded
@POST("/api/unlock")
Call<String> unLockThing(@HeaderMap Map<String, String> headers, @FieldMap Map<String, String> params);
}

View File

@ -4,15 +4,18 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources
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
import org.checkerframework.checker.units.qual.A
private const val EXTRA_POST = "EP"
private const val EXTRA_POSITION = "EPO"
/**
* A simple [Fragment] subclass.
@ -21,11 +24,13 @@ private const val EXTRA_POST = "EP"
*/
class ModerationActionBottomSheetFragment : LandscapeExpandedRoundedBottomSheetDialogFragment() {
private var post: Post? = null
private var position: Int = -1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
post = it.getParcelable(EXTRA_POST)
position = it.getInt(EXTRA_POSITION, -1)
}
}
@ -33,13 +38,45 @@ class ModerationActionBottomSheetFragment : LandscapeExpandedRoundedBottomSheetD
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()
post?.let { post ->
if (parentFragment is PostModerationActionHandler) {
binding.approveTextViewModerationActionBottomSheetFragment.setOnClickListener {
(parentFragment as PostModerationActionHandler).approvePost(post, position)
dismiss()
}
binding.removeTextViewModerationActionBottomSheetFragment.setOnClickListener {
(parentFragment as PostModerationActionHandler).removePost(post, position, false)
dismiss()
}
binding.spamTextViewModerationActionBottomSheetFragment.setOnClickListener {
(parentFragment as PostModerationActionHandler).removePost(post, position, true)
dismiss()
}
binding.toggleStickyTextViewModerationActionBottomSheetFragment.setText(if (post.isStickied) R.string.unset_sticky_post else R.string.set_sticky_post)
activity?.let {
binding.toggleStickyTextViewModerationActionBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds(
AppCompatResources.getDrawable(it, if (post.isStickied) R.drawable.ic_unstick_post_24dp else R.drawable.ic_stick_post_24dp), null, null, null
)
}
binding.toggleStickyTextViewModerationActionBottomSheetFragment.setOnClickListener {
(parentFragment as PostModerationActionHandler).toggleSticky(post, position)
dismiss()
}
binding.toggleLockTextViewModerationActionBottomSheetFragment.setText(if (post.isLocked) R.string.unlock else R.string.lock)
activity?.let {
binding.toggleLockTextViewModerationActionBottomSheetFragment.setCompoundDrawablesWithIntrinsicBounds(
AppCompatResources.getDrawable(it, if (post.isLocked) R.drawable.ic_unlock_24dp else R.drawable.ic_lock_day_night_24dp), null, null, null
)
}
binding.toggleLockTextViewModerationActionBottomSheetFragment.setOnClickListener {
(parentFragment as PostModerationActionHandler).toggleLock(post, position)
dismiss()
}
}
}
return binding.root
@ -47,10 +84,11 @@ class ModerationActionBottomSheetFragment : LandscapeExpandedRoundedBottomSheetD
companion object {
@JvmStatic
fun newInstance(post: Post) =
fun newInstance(post: Post, position: Int) =
ModerationActionBottomSheetFragment().apply {
arguments = Bundle().apply {
putParcelable(EXTRA_POST, post)
putInt(EXTRA_POSITION, position)
}
}
}

View File

@ -280,7 +280,7 @@ public class PostOptionsBottomSheetFragment extends LandscapeExpandedRoundedBott
if (mPost.isCanModPost()) {
binding.modTextViewPostOptionsBottomSheetFragment.setVisibility(View.VISIBLE);
binding.modTextViewPostOptionsBottomSheetFragment.setOnClickListener(view -> {
ModerationActionBottomSheetFragment moderationActionBottomSheetFragment = ModerationActionBottomSheetFragment.newInstance(mPost);
ModerationActionBottomSheetFragment moderationActionBottomSheetFragment = ModerationActionBottomSheetFragment.newInstance(mPost, getArguments().getInt(EXTRA_POST_LIST_POSITION, 0));
Fragment parentFragment = getParentFragment();
if (parentFragment != null) {
moderationActionBottomSheetFragment.show(parentFragment.getChildFragmentManager(), moderationActionBottomSheetFragment.getTag());

View File

@ -18,7 +18,6 @@ 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;
@ -60,8 +59,8 @@ import ml.docilealligator.infinityforreddit.events.ChangeDefaultPostLayoutEvent;
import ml.docilealligator.infinityforreddit.events.ChangeNetworkStatusEvent;
import ml.docilealligator.infinityforreddit.events.ChangeSavePostFeedScrolledPositionEvent;
import ml.docilealligator.infinityforreddit.events.NeedForPostListFromPostFragmentEvent;
import ml.docilealligator.infinityforreddit.events.PostUpdateEventToPostList;
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;
@ -881,11 +880,9 @@ 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();
}
mPostViewModel.moderationEventLiveData.observe(getViewLifecycleOwner(), moderationEvent -> {
EventBus.getDefault().post(new PostUpdateEventToPostList(moderationEvent.getPost(), moderationEvent.getPosition()));
Toast.makeText(activity, moderationEvent.getToastMessageResId(), Toast.LENGTH_SHORT).show();
});
mAdapter.addLoadStateListener(combinedLoadStates -> {
@ -1395,7 +1392,22 @@ public class PostFragment extends PostFragmentBase implements FragmentCommunicat
}
@Override
public void approvePost(@Nullable Post post) {
mPostViewModel.approvePost(post);
public void approvePost(@NonNull Post post, int position) {
mPostViewModel.approvePost(post, position);
}
@Override
public void removePost(@NonNull Post post, int position, boolean isSpam) {
mPostViewModel.removePost(post, position, isSpam);
}
@Override
public void toggleSticky(@NonNull Post post, int position) {
mPostViewModel.toggleStickyPost(post, position);
}
@Override
public void toggleLock(@NonNull Post post, int position) {
mPostViewModel.toggleLockPost(post, position);
}
}

View File

@ -622,6 +622,8 @@ public abstract class PostFragmentBase extends Fragment {
post.setSpoiler(event.post.isSpoiler());
post.setFlair(event.post.getFlair());
post.setSaved(event.post.isSaved());
post.setIsStickied(event.post.isStickied());
post.setIsLocked(event.post.isLocked());
if (event.post.isRead()) {
post.markAsRead();
}

View File

@ -1,5 +1,27 @@
package ml.docilealligator.infinityforreddit.moderation
enum class ModerationEvent {
APPROVED, APPROVE_FAILED, REMOVED, REMOVE_FAILED, MARKED_AS_SPAM, MARK_AS_SPAM_FAILED
import ml.docilealligator.infinityforreddit.R
import ml.docilealligator.infinityforreddit.post.Post
sealed class ModerationEvent(open val post: Post, open val position: Int, val toastMessageResId: Int) {
data class Approved(override val post: Post, override val position: Int) : ModerationEvent(post, position, R.string.approved)
data class ApproveFailed(override val post: Post, override val position: Int) : ModerationEvent(post, position, R.string.approve_failed)
data class Removed(override val post: Post, override val position: Int) : ModerationEvent(post, position, R.string.removed)
data class RemoveFailed(override val post: Post, override val position: Int) : ModerationEvent(post, position, R.string.remove_failed)
data class MarkedAsSpam(override val post: Post, override val position: Int) : ModerationEvent(post, position, R.string.marked_as_spam)
data class MarkAsSpamFailed(override val post: Post, override val position: Int) : ModerationEvent(post, position, R.string.mark_as_spam_failed)
data class SetStickyPost(override val post: Post, override val position: Int) : ModerationEvent(post, position, R.string.set_sticky_post)
data class SetStickyPostFailed(override val post: Post, override val position: Int) : ModerationEvent(post, position, R.string.set_sticky_post_failed)
data class UnsetStickyPost(override val post: Post, override val position: Int) : ModerationEvent(post, position, R.string.unset_sticky_post)
data class UnsetStickyPostFailed(override val post: Post, override val position: Int) : ModerationEvent(post, position, R.string.unset_sticky_post_failed)
data class Locked(override val post: Post, override val position: Int) : ModerationEvent(post, position, R.string.locked)
data class LockFailed(override val post: Post, override val position: Int) : ModerationEvent(post, position, R.string.lock_failed)
data class Unlocked(override val post: Post, override val position: Int) : ModerationEvent(post, position, R.string.unlocked)
data class UnlockFailed(override val post: Post, override val position: Int) : ModerationEvent(post, position, R.string.unlock_failed)
}

View File

@ -62,9 +62,9 @@ public class Post implements Parcelable {
private boolean hidden;
private boolean spoiler;
private boolean nsfw;
private final boolean stickied;
private boolean stickied;
private final boolean archived;
private final boolean locked;
private boolean locked;
private boolean saved;
private final boolean isCrosspost;
private boolean isRead;
@ -542,6 +542,10 @@ public class Post implements Parcelable {
return stickied;
}
public void setIsStickied(boolean value) {
stickied = value;
}
public boolean isArchived() {
return archived;
}
@ -550,6 +554,10 @@ public class Post implements Parcelable {
return locked;
}
public void setIsLocked(boolean value) {
locked = value;
}
public boolean isSaved() {
return saved;
}

View File

@ -494,27 +494,88 @@ public class PostViewModel extends ViewModel {
}
}
public void approvePost(@Nullable Post post) {
if (post == null) {
moderationEventLiveData.postValue(ModerationEvent.APPROVE_FAILED);
return;
}
public void approvePost(@NonNull Post post, int position) {
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);
moderationEventLiveData.postValue(new ModerationEvent.Approved(post, position));
} else {
moderationEventLiveData.postValue(ModerationEvent.APPROVE_FAILED);
moderationEventLiveData.postValue(new ModerationEvent.ApproveFailed(post, position));
}
}
@Override
public void onFailure(@NonNull Call<String> call, @NonNull Throwable throwable) {
moderationEventLiveData.postValue(ModerationEvent.APPROVE_FAILED);
moderationEventLiveData.postValue(new ModerationEvent.ApproveFailed(post, position));
}
});
}
public void removePost(@NonNull Post post, int position, boolean isSpam) {
Map<String, String> params = new HashMap<>();
params.put(APIUtils.ID_KEY, post.getFullName());
params.put(APIUtils.SPAM_KEY, Boolean.toString(isSpam));
retrofit.create(RedditAPI.class).removeThing(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback<>() {
@Override
public void onResponse(@NonNull Call<String> call, @NonNull Response<String> response) {
if (response.isSuccessful()) {
moderationEventLiveData.postValue(isSpam ? new ModerationEvent.MarkedAsSpam(post, position): new ModerationEvent.Removed(post, position));
} else {
moderationEventLiveData.postValue(isSpam ? new ModerationEvent.MarkAsSpamFailed(post, position) : new ModerationEvent.RemoveFailed(post, position));
}
}
@Override
public void onFailure(@NonNull Call<String> call, @NonNull Throwable throwable) {
moderationEventLiveData.postValue(isSpam ? new ModerationEvent.MarkAsSpamFailed(post, position) : new ModerationEvent.RemoveFailed(post, position));
}
});
}
public void toggleStickyPost(@NonNull Post post, int position) {
Map<String, String> params = new HashMap<>();
params.put(APIUtils.ID_KEY, post.getFullName());
params.put(APIUtils.STATE_KEY, Boolean.toString(!post.isStickied()));
params.put(APIUtils.API_TYPE_KEY, APIUtils.API_TYPE_JSON);
retrofit.create(RedditAPI.class).toggleStickyPost(APIUtils.getOAuthHeader(accessToken), params).enqueue(new Callback<>() {
@Override
public void onResponse(@NonNull Call<String> call, @NonNull Response<String> response) {
if (response.isSuccessful()) {
post.setIsStickied(!post.isStickied());
moderationEventLiveData.postValue(post.isStickied() ? new ModerationEvent.SetStickyPost(post, position): new ModerationEvent.UnsetStickyPost(post, position));
} else {
moderationEventLiveData.postValue(post.isStickied() ? new ModerationEvent.UnsetStickyPostFailed(post, position) : new ModerationEvent.SetStickyPostFailed(post, position));
}
}
@Override
public void onFailure(@NonNull Call<String> call, @NonNull Throwable throwable) {
moderationEventLiveData.postValue(post.isStickied() ? new ModerationEvent.UnsetStickyPostFailed(post, position) : new ModerationEvent.SetStickyPostFailed(post, position));
}
});
}
public void toggleLockPost(@NonNull Post post, int position) {
Map<String, String> params = new HashMap<>();
params.put(APIUtils.ID_KEY, post.getFullName());
Call<String> call = post.isLocked() ? retrofit.create(RedditAPI.class).unLockThing(APIUtils.getOAuthHeader(accessToken), params) : retrofit.create(RedditAPI.class).lockThing(APIUtils.getOAuthHeader(accessToken), params);
call.enqueue(new Callback<>() {
@Override
public void onResponse(@NonNull Call<String> call, @NonNull Response<String> response) {
if (response.isSuccessful()) {
post.setIsLocked(!post.isLocked());
moderationEventLiveData.postValue(post.isLocked() ? new ModerationEvent.Locked(post, position): new ModerationEvent.Unlocked(post, position));
} else {
moderationEventLiveData.postValue(post.isLocked() ? new ModerationEvent.UnlockFailed(post, position) : new ModerationEvent.LockFailed(post, position));
}
}
@Override
public void onFailure(@NonNull Call<String> call, @NonNull Throwable throwable) {
moderationEventLiveData.postValue(post.isLocked() ? new ModerationEvent.UnlockFailed(post, position) : new ModerationEvent.LockFailed(post, position));
}
});
}

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="M640,200v280l68,68q6,6 9,13.5t3,15.5v23q0,17 -11.5,28.5T680,640L520,640v234q0,17 -11.5,28.5T480,914q-17,0 -28.5,-11.5T440,874v-234L280,640q-17,0 -28.5,-11.5T240,600v-23q0,-8 3,-15.5t9,-13.5l68,-68v-280q-17,0 -28.5,-11.5T280,160q0,-17 11.5,-28.5T320,120h320q17,0 28.5,11.5T680,160q0,17 -11.5,28.5T640,200ZM354,560h252l-46,-46v-314L400,200v314l-46,46ZM480,560Z"
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="M240,800h480v-400L240,400v400ZM480,680q33,0 56.5,-23.5T560,600q0,-33 -23.5,-56.5T480,520q-33,0 -56.5,23.5T400,600q0,33 23.5,56.5T480,680ZM240,800v-400,400ZM240,880q-33,0 -56.5,-23.5T160,800v-400q0,-33 23.5,-56.5T240,320h280v-80q0,-83 58.5,-141.5T720,40q71,0 124,43t70,108q5,17 -6.5,33T880,240q-17,0 -28,-7t-16,-23q-11,-38 -42.5,-64T720,120q-50,0 -85,35t-35,85v80h120q33,0 56.5,23.5T800,400v400q0,33 -23.5,56.5T720,880L240,880Z"
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="M560,200L400,200v87L290,177q-5,-5 -7.5,-11t-2.5,-12q0,-13 9,-23.5t24,-10.5h327q17,0 28.5,11.5T680,160q0,16 -14.5,22.5T640,200v240q0,17 -11.5,28.5T600,480q-17,0 -28.5,-11.5T560,440v-240ZM440,880v-240L296,640q-25,0 -40,-17.5T241,583q0,-11 4.5,-22t14.5,-21l60,-60v-46L84,196q-11,-11 -11.5,-27.5T84,140q11,-11 28,-11t28,11l679,679q12,12 11.5,28.5T818,876q-12,11 -28,11.5T762,876L526,640h-6v240q0,17 -11.5,28.5T480,920q-17,0 -28.5,-11.5T440,880ZM354,560h92l-44,-44 -2,-2 -46,46ZM480,367ZM402,516Z"
android:fillColor="#000000"/>
</vector>

View File

@ -71,6 +71,43 @@
android:textSize="?attr/font_default"
app:drawableTint="?attr/primaryTextColor" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/toggle_sticky_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: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" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/toggle_lock_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:drawablePadding="48dp"
android:focusable="true"
android:fontFamily="?attr/font_family"
android:paddingStart="32dp"
android:paddingTop="16dp"
android:paddingEnd="32dp"
android:paddingBottom="16dp"
android:textColor="?attr/primaryTextColor"
android:textSize="?attr/font_default"
app:drawableTint="?attr/primaryTextColor" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -1552,5 +1552,18 @@
<string name="approved">Approved</string>
<string name="approve_failed">Approve failed</string>
<string name="remove">Remove</string>
<string name="removed">Removed</string>
<string name="remove_failed">Remove failed</string>
<string name="mark_as_spam">Mark as spam</string>
<string name="marked_as_spam">Marked as spam</string>
<string name="mark_as_spam_failed">Mark as spam failed</string>
<string name="set_sticky_post">Set sticky post</string>
<string name="set_sticky_post_failed">Set sticky post failed</string>
<string name="unset_sticky_post">Unset sticky post</string>
<string name="unset_sticky_post_failed">Unset sticky post failed</string>
<string name="lock">Lock</string>
<string name="locked">Locked</string>
<string name="lock_failed">Lock failed</string>
<string name="unlocked">Unlocked</string>
<string name="unlock_failed">Unlock failed</string>
</resources>