diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/NavigationEvent.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/NavigationEvent.kt index e4408e169..f61212c39 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/NavigationEvent.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/NavigationEvent.kt @@ -11,6 +11,7 @@ import com.ferg.awfulapp.search.SearchFilter import com.ferg.awfulapp.search.SearchFragment import com.ferg.awfulapp.thread.AwfulURL import com.ferg.awfulapp.users.LepersColonyFragment +import com.ferg.awfulapp.users.ModQueueRequestFragment import com.ferg.awfulapp.util.AwfulUtils import com.ferg.awfulapp.util.tryGetIntExtra import timber.log.Timber @@ -135,6 +136,24 @@ sealed class NavigationEvent(private val extraTypeId: String) { } } + /** + * Show the modqueue probation/ban request page for a post ([ban] selects which), targeting + * the [userId] who made the post [postId] in thread [threadId]. + */ + data class ModQueueRequest(val ban: Boolean, val userId: Int, val postId: Int, val threadId: Int) : NavigationEvent(TYPE_MODQUEUE_REQUEST) { + + override fun activityIntent(context: Context) = + BasicActivity.intentFor(ModQueueRequestFragment::class.java, context, + context.getString(if (ban) R.string.mod_ban_request_title else R.string.mod_probation_request_title)) + + override val addDataToIntent: Intent.() -> Unit = { + putExtra(KEY_MODQUEUE_IS_BAN, ban) + putExtra(Constants.PARAM_USER_ID, userId) + putExtra(Constants.PARAM_POST_ID, postId) + putExtra(Constants.PARAM_THREAD_ID, threadId) + } + } + /** * Build an Intent for this NavigationEvent. @@ -168,8 +187,10 @@ sealed class NavigationEvent(private val extraTypeId: String) { private const val TYPE_COMPOSE_PRIVATE_MESSAGE = "nav_compose_private_message" private const val TYPE_ANNOUNCEMENTS = "nav_announcements" private const val TYPE_LEPERS_COLONY = "nav_rap_sheet" + private const val TYPE_MODQUEUE_REQUEST = "nav_modqueue_request" private const val KEY_SEARCH_FILTERS = "key_search_filters" + private const val KEY_MODQUEUE_IS_BAN = "key_modqueue_is_ban" private fun Context.intentFor(clazz: Class): Intent = Intent().setClass(this, clazz) @@ -211,6 +232,12 @@ sealed class NavigationEvent(private val extraTypeId: String) { userId = getIntExtra(Constants.PARAM_USER_ID), page = getIntExtra(Constants.PARAM_PAGE) ?: LepersColonyFragment.FIRST_PAGE ) + TYPE_MODQUEUE_REQUEST -> ModQueueRequest( + ban = getBooleanExtra(KEY_MODQUEUE_IS_BAN, false), + userId = getIntExtra(Constants.PARAM_USER_ID)!!, + postId = getIntExtra(Constants.PARAM_POST_ID)!!, + threadId = getIntExtra(Constants.PARAM_THREAD_ID)!! + ) else -> { Timber.w("Couldn't parse Intent as NavigationEvent - event key: ${getStringExtra(EVENT_EXTRA_KEY)}") MainActivity @@ -244,6 +271,8 @@ sealed class NavigationEvent(private val extraTypeId: String) { ForumIndex isBanlist -> LepersColony(id.toInt()) + isModQueueRequest -> + ModQueueRequest(isBanRequest, id.toInt(), targetPostId.toInt(), threadId.toInt()) else -> null } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java b/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java index 6b9bd09a8..d91a4ced8 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/ThreadDisplayFragment.java @@ -377,6 +377,10 @@ public boolean shouldOverrideUrlLoading(WebView aView, String aUrl) { case BANLIST: navigate(new NavigationEvent.LepersColony((int) aLink.getId())); break; + case MODQUEUE_REQUEST: + navigate(new NavigationEvent.ModQueueRequest(aLink.isBanRequest(), + (int) aLink.getId(), (int) aLink.getTargetPostId(), (int) aLink.getThreadId())); + break; case INDEX: navigate(NavigationEvent.ForumIndex.INSTANCE); break; @@ -1180,6 +1184,7 @@ public void onMoreClick( final String aUserId, final String lastReadUrl, final boolean editable, + final boolean hasModControls, final String posterRole, final boolean isPlat, final String avatarUrl) { @@ -1187,7 +1192,9 @@ public void onMoreClick( PostContextMenu postActions = PostContextMenu.newInstance(getThreadId(), Integer.parseInt(aPostId), Integer.parseInt(lastReadUrl), - editable, aUsername, + editable, + hasModControls, + aUsername, Integer.parseInt(aUserId), isPlat, posterRole, diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/constants/Constants.java b/Awful.apk/src/main/java/com/ferg/awfulapp/constants/Constants.java index f4a919394..ba5674f78 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/constants/Constants.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/constants/Constants.java @@ -54,6 +54,7 @@ public class Constants { public static final String FUNCTION_RATE_THREAD = BASE_URL + "/threadrate.php"; public static final String FUNCTION_MISC = BASE_URL + "/misc.php"; public static final String FUNCTION_REPORT = BASE_URL + "/modalert.php"; + public static final String FUNCTION_MODQUEUE = BASE_URL + "/modqueue.php"; public static final String FUNCTION_POSTINGS = BASE_URL + "/postings.php"; public static final String FUNCTION_NEW_THREAD = BASE_URL + "/newthread.php"; @@ -62,6 +63,7 @@ public class Constants { public static final String PATH_BOOKMARKS = "bookmarkthreads.php"; public static final String PATH_USERCP = "usercp.php"; public static final String PATH_BANLIST = "banlist.php"; + public static final String PATH_MODQUEUE = "modqueue.php"; public static final String ACTION_PROFILE = "getinfo"; public static final String ACTION_SEARCH_POST_HISTORY = "do_search_posthistory"; @@ -72,6 +74,8 @@ public class Constants { public static final String ACTION_QUERY = "query"; public static final String ACTION_RESULTS = "results"; public static final String ACTION_TOGGLE_THREAD_LOCKED = "openclosethread"; + public static final String ACTION_REQUEST_PROBATION = "request_probation"; + public static final String ACTION_REQUEST_BAN = "request_ban"; public static final String PARAM_USER_ID = "userid"; public static final String PARAM_USERNAME = "username"; @@ -87,6 +91,7 @@ public class Constants { public static final String PARAM_PRIVATE_MESSAGE_ID = "privatemessageid"; public static final String PARAM_VOTE = "vote"; public static final String PARAM_POST_ID = "postid"; + public static final String PARAM_TARGET_POST_ID = "tpostid"; public static final String PARAM_USERLIST = "userlist"; public static final String PARAM_FORMKEY = "formkey"; public static final String PARAM_FORM_COOKIE = "form_cookie"; diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/ModeratePostMenu.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/ModeratePostMenu.kt new file mode 100644 index 000000000..6dc1ba70b --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/ModeratePostMenu.kt @@ -0,0 +1,102 @@ +package com.ferg.awfulapp.popupmenu + +import android.os.Bundle +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.ferg.awfulapp.AwfulActivity +import com.ferg.awfulapp.NavigationEvent +import com.ferg.awfulapp.R +import com.ferg.awfulapp.ThreadDisplayFragment +import com.ferg.awfulapp.popupmenu.ModeratePostMenu.ModerateMenuAction.EDIT_POST +import com.ferg.awfulapp.popupmenu.ModeratePostMenu.ModerateMenuAction.REQUEST_BAN +import com.ferg.awfulapp.popupmenu.ModeratePostMenu.ModerateMenuAction.REQUEST_PROBATION +import com.ferg.awfulapp.thread.AwfulMessage + +/** + * Context menu with moderation actions for someone else's post. + * + * Shows an edit option when the post is editable by the current user, plus the modqueue + * probation/ban request options when the post carries modqueue controls. + */ +class ModeratePostMenu : BasePopupMenu() { + + private var threadId = 0 + private var postId = 0 + private var posterUserId = 0 + private var editable = false + private var hasModControls = false + + companion object { + @JvmField + val TAG: String = ModeratePostMenu::class.java.simpleName + + private const val ARG_THREAD_ID = "threadId" + private const val ARG_POST_ID = "postId" + private const val ARG_POSTER_USER_ID = "posterUserId" + private const val ARG_EDITABLE = "editable" + private const val ARG_HAS_MOD_CONTROLS = "hasModControls" + + /** + * Get a moderation menu for a post, where [editable] adds the edit option and + * [hasModControls] adds the modqueue probation/ban request options. + */ + @JvmStatic + fun newInstance(threadId: Int, postId: Int, posterUserId: Int, editable: Boolean, hasModControls: Boolean) = + ModeratePostMenu().apply { + arguments = Bundle().apply { + putInt(ARG_THREAD_ID, threadId) + putInt(ARG_POST_ID, postId) + putInt(ARG_POSTER_USER_ID, posterUserId) + putBoolean(ARG_EDITABLE, editable) + putBoolean(ARG_HAS_MOD_CONTROLS, hasModControls) + } + } + } + + override fun init(args: Bundle) = with(args) { + threadId = getInt(ARG_THREAD_ID) + postId = getInt(ARG_POST_ID) + posterUserId = getInt(ARG_POSTER_USER_ID) + editable = getBoolean(ARG_EDITABLE) + hasModControls = getBoolean(ARG_HAS_MOD_CONTROLS) + } + + override fun generateMenuItems() = + mutableListOf() + .apply { if (editable) add(EDIT_POST) } + .apply { if (hasModControls) { add(REQUEST_PROBATION); add(REQUEST_BAN) } } + + override fun getMenuLabel(action: ModerateMenuAction): String = getString(action.menuLabelRes) + + override fun getTitle(): String = getString(R.string.action_moderate_post) + + override fun onActionClicked(action: ModerateMenuAction) { + when (action) { + EDIT_POST -> + (targetFragment as? ThreadDisplayFragment) + ?.displayPostReplyDialog(threadId, postId, AwfulMessage.TYPE_EDIT) + REQUEST_PROBATION -> + navigateTo(NavigationEvent.ModQueueRequest(ban = false, userId = posterUserId, postId = postId, threadId = threadId)) + REQUEST_BAN -> + navigateTo(NavigationEvent.ModQueueRequest(ban = true, userId = posterUserId, postId = postId, threadId = threadId)) + } + } + + private fun navigateTo(event: NavigationEvent) { + (activity as AwfulActivity?)?.navigate(event) + } + + enum class ModerateMenuAction( + @DrawableRes private val iconResId: Int, + @StringRes val menuLabelRes: Int + ) : AwfulAction { + + EDIT_POST(R.drawable.ic_create_dark, R.string.action_edit_post), + REQUEST_PROBATION(R.drawable.ic_history_dark_24dp, R.string.action_request_probation), + REQUEST_BAN(R.drawable.ic_gavel_dark_24dp, R.string.action_request_ban); + + override fun getIconId() = iconResId + // labels are resolved from menuLabelRes in getMenuLabel above, where a Context is available + override fun getMenuLabel() = "" + } +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/PostContextMenu.java b/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/PostContextMenu.java index c4ddcfe66..818c330e4 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/PostContextMenu.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/popupmenu/PostContextMenu.java @@ -23,6 +23,7 @@ import static com.ferg.awfulapp.popupmenu.PostContextMenu.PostMenuAction.IGNORE_USER; import static com.ferg.awfulapp.popupmenu.PostContextMenu.PostMenuAction.MARK_LAST_SEEN; import static com.ferg.awfulapp.popupmenu.PostContextMenu.PostMenuAction.MARK_USER; +import static com.ferg.awfulapp.popupmenu.PostContextMenu.PostMenuAction.MODERATE; import static com.ferg.awfulapp.popupmenu.PostContextMenu.PostMenuAction.RAP_SHEET; import static com.ferg.awfulapp.popupmenu.PostContextMenu.PostMenuAction.REPORT_POST; import static com.ferg.awfulapp.popupmenu.PostContextMenu.PostMenuAction.SEND_PM; @@ -42,6 +43,7 @@ public class PostContextMenu extends BasePopupMenu generateMenuItems() { List awfulActions = new ArrayList<>(); awfulActions.add(PostMenuAction.QUOTE); - if (editable) { + if (editable && ownPost) { awfulActions.add(EDIT); } awfulActions.add(MARK_LAST_SEEN); @@ -158,6 +165,9 @@ List generateMenuItems() { awfulActions.add(prefs.blockedAvatarUrls.contains(posterAvatarUrl) ? SHOW_AVATAR : HIDE_AVATAR); } + if (!ownPost && (editable || hasModControls)) { + awfulActions.add(MODERATE); + } return awfulActions; } @@ -212,6 +222,12 @@ void onActionClicked(@NonNull PostMenuAction action) { case HIDE_AVATAR: parent.toggleAvatar(posterAvatarUrl); break; + case MODERATE: + ModeratePostMenu moderateMenu = ModeratePostMenu.newInstance(threadId, postId, + posterUserId, editable, hasModControls); + moderateMenu.setTargetFragment(parent, -1); + moderateMenu.show(parent.getParentFragmentManager(), ModeratePostMenu.TAG); + break; } } @@ -219,6 +235,9 @@ void onActionClicked(@NonNull PostMenuAction action) { @NonNull @Override String getMenuLabel(@NonNull PostMenuAction action) { + if (action == MODERATE) { + return getString(R.string.action_moderate_post); + } // need to add the post's username to some of these return String.format(action.getMenuLabel(), posterUsername); } @@ -244,7 +263,9 @@ public enum PostMenuAction implements AwfulAction { RAP_SHEET(R.drawable.ic_gavel_dark_24dp, "%s's rap sheet"), IGNORE_USER(R.drawable.ic_ignore_dark, "Ignore %s"), SHOW_AVATAR(R.drawable.ic_visibility_dark, "Show avatar of %s"), - HIDE_AVATAR(R.drawable.ic_ignore_dark, "Hide avatar of %s"); + HIDE_AVATAR(R.drawable.ic_ignore_dark, "Hide avatar of %s"), + // label comes from R.string.action_moderate_post, see #getMenuLabel + MODERATE(R.drawable.ic_info_dark, ""); final int iconId; @NonNull diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/provider/AwfulProvider.java b/Awful.apk/src/main/java/com/ferg/awfulapp/provider/AwfulProvider.java index 17c7eb856..d6f8acdc5 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/provider/AwfulProvider.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/provider/AwfulProvider.java @@ -187,6 +187,7 @@ private static String[] arrayOfKeys(@NonNull Map map) { sPostProjectionMap.put(AwfulPost.IS_IGNORED, AwfulPost.IS_IGNORED); sPostProjectionMap.put(AwfulPost.PREVIOUSLY_READ, AwfulPost.PREVIOUSLY_READ); sPostProjectionMap.put(AwfulPost.EDITABLE, AwfulPost.EDITABLE); + sPostProjectionMap.put(AwfulPost.HAS_MOD_CONTROLS, AwfulPost.HAS_MOD_CONTROLS); sPostProjectionMap.put(AwfulPost.IS_OP, AwfulPost.IS_OP); sPostProjectionMap.put(AwfulPost.IS_PLAT, AwfulPost.IS_PLAT); sPostProjectionMap.put(AwfulPost.ROLE, AwfulPost.ROLE); diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/provider/DatabaseHelper.java b/Awful.apk/src/main/java/com/ferg/awfulapp/provider/DatabaseHelper.java index f3768db61..fb779a8ee 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/provider/DatabaseHelper.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/provider/DatabaseHelper.java @@ -20,7 +20,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "awful.db"; - private static final int DATABASE_VERSION = 39; + private static final int DATABASE_VERSION = 40; static final String TABLE_FORUM = "forum"; public static final String TABLE_THREADS = "threads"; @@ -109,6 +109,7 @@ private void createPostTable(SQLiteDatabase aDb) { AwfulPost.IS_IGNORED + " INTEGER," + AwfulPost.PREVIOUSLY_READ + " INTEGER," + AwfulPost.EDITABLE + " INTEGER," + + AwfulPost.HAS_MOD_CONTROLS + " INTEGER," + AwfulPost.IS_OP + " INTEGER," + AwfulPost.IS_PLAT + " INTEGER," + AwfulPost.ROLE + " VARCHAR," + @@ -215,6 +216,8 @@ public void onUpgrade(SQLiteDatabase aDb, int aOldVersion, int aNewVersion) { createPostTable(aDb); case 38: aDb.execSQL("ALTER TABLE " + TABLE_THREADS + " ADD COLUMN " + AwfulThread.LAST_POST_DATE + " INTEGER DEFAULT 0"); + case 39: + aDb.execSQL("ALTER TABLE " + TABLE_POSTS + " ADD COLUMN " + AwfulPost.HAS_MOD_CONTROLS + " INTEGER DEFAULT 0"); break;//make sure to keep this break statement on the last case of this switch default: wipeRecreateTables(aDb); diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/task/ModQueueRequest.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ModQueueRequest.kt new file mode 100644 index 000000000..b0216711d --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/task/ModQueueRequest.kt @@ -0,0 +1,93 @@ +package com.ferg.awfulapp.task + +import android.content.Context +import com.ferg.awfulapp.R +import com.ferg.awfulapp.constants.Constants.ACTION_REQUEST_BAN +import com.ferg.awfulapp.constants.Constants.ACTION_REQUEST_PROBATION +import com.ferg.awfulapp.constants.Constants.FUNCTION_MODQUEUE +import com.ferg.awfulapp.constants.Constants.PARAM_ACTION +import com.ferg.awfulapp.constants.Constants.PARAM_TARGET_POST_ID +import com.ferg.awfulapp.constants.Constants.PARAM_THREAD_ID +import com.ferg.awfulapp.constants.Constants.PARAM_USER_ID +import com.ferg.awfulapp.util.AwfulError +import org.jsoup.nodes.Document + +/** + * Request to load the modqueue probation/ban request page for a post, parsing out the details + * needed to display the form and submit it through a [ModQueueSubmitRequest]. + */ +class ModQueueFormRequest(context: Context, ban: Boolean, userId: Int, postId: Int, threadId: Int) : + AwfulRequest(context, FUNCTION_MODQUEUE) { + + companion object { + const val FIELD_DURATION = "data" + const val FIELD_BAN_TYPE = "act" + const val FIELD_REASON = "reason" + const val FIELD_NOTES = "notes" + } + + init { + with(parameters) { + add(PARAM_ACTION, if (ban) ACTION_REQUEST_BAN else ACTION_REQUEST_PROBATION) + add(PARAM_USER_ID, userId.toString()) + add(PARAM_TARGET_POST_ID, postId.toString()) + add(PARAM_THREAD_ID, threadId.toString()) + } + } + + @Throws(AwfulError::class) + override fun handleResponse(doc: Document): ModQueueForm { + val form = doc.selectFirst("form[action=modqueue.php]") + ?: throw AwfulError(context.getString(R.string.mod_request_form_load_failed)) + + val hiddenFields = form.select("input[type=hidden]") + .associate { it.attr("name") to it.attr("value") } + + fun tableValueFor(label: String) = + form.select("td").firstOrNull { it.text().trim() == label }?.nextElementSibling() + + val username = tableValueFor("User:")?.text()?.trim() + val history = tableValueFor("History:")?.run { + select("a").remove() + text().replace("""\(\s*\)""".toRegex(), "").trim() + } + + val durations = form.select("select[name=$FIELD_DURATION] option") + .map { Choice(it.`val`(), it.text().trim(), it.hasAttr("selected")) } + val banTypes = form.select("input[name=$FIELD_BAN_TYPE][type=radio]") + .map { Choice(it.`val`(), it.nextElementSibling()?.text()?.trim() ?: "", it.hasAttr("checked")) } + + return ModQueueForm(username, history, durations, banTypes, hiddenFields) + } + + /** A selectable option in the form, e.g. a probation duration or ban type */ + data class Choice(val value: String, val label: String, val selected: Boolean) + + /** + * The pertinent parts of a modqueue request page: the target [username] and punishment + * [history], the available [durations] (probation) or [banTypes] (ban), and the form's + * [hiddenFields] which need to be sent back on submission. + */ + data class ModQueueForm( + val username: String?, + val history: String?, + val durations: List, + val banTypes: List, + val hiddenFields: Map + ) +} + + +/** + * Submits a modqueue probation/ban request. [params] should hold the form's hidden fields plus + * the user's input, as parsed from a [ModQueueFormRequest.ModQueueForm]. + */ +class ModQueueSubmitRequest(context: Context, params: Map) : + AwfulRequest(context, FUNCTION_MODQUEUE, isPostRequest = true) { + + init { + params.forEach { (name, value) -> parameters.add(name, value) } + } + + override fun handleResponse(doc: Document): Void? = null +} diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulHtmlPage.java b/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulHtmlPage.java index f24a30aaa..9f5df4d99 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulHtmlPage.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulHtmlPage.java @@ -226,6 +226,7 @@ private static String getPostsHtml(List aPosts, AwfulPreferences aPre postData.put("avatarText", post.getAvatarText()); postData.put("lastReadUrl", post.getLastReadUrl()); postData.put("editable", post.isEditable() ? "editable" : null); + postData.put("modControls", post.hasModControls() ? "modControls" : null); postData.put("postcontent", post.getContent()); postData.put("hideAvatar", aPrefs.isBlockedAvatar(avatar) ? "blockedAvatar" : null); diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulPost.java b/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulPost.java index 89bb949a9..68f6d65d7 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulPost.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulPost.java @@ -87,6 +87,7 @@ public class AwfulPost { public static final String IS_IGNORED = "is_ignored"; public static final String PREVIOUSLY_READ = "previously_read"; public static final String EDITABLE = "editable"; + public static final String HAS_MOD_CONTROLS = "has_mod_controls"; public static final String IS_OP = "is_op"; public static final String IS_PLAT = "is_plat"; public static final String ROLE = "role"; @@ -126,6 +127,7 @@ public class AwfulPost { private boolean mPreviouslyRead = false; private String mLastReadUrl = ""; private boolean mEditable; + private boolean mHasModControls = false; private boolean isOp = false; private boolean isPlat = false; private String mRole = ""; @@ -260,6 +262,7 @@ public static ArrayList fromCursor(Context aContext, Cursor aCursor) int isIgnoredIndex = aCursor.getColumnIndex(IS_IGNORED); int previouslyReadIndex = aCursor.getColumnIndex(PREVIOUSLY_READ); int editableIndex = aCursor.getColumnIndex(EDITABLE); + int hasModControlsIndex = aCursor.getColumnIndex(HAS_MOD_CONTROLS); int isOpIndex = aCursor.getColumnIndex(IS_OP); int isPlatIndex = aCursor.getColumnIndex(IS_PLAT); int roleIndex = aCursor.getColumnIndex(ROLE); @@ -284,6 +287,7 @@ public static ArrayList fromCursor(Context aContext, Cursor aCursor) current.setPreviouslyRead(aCursor.getInt(previouslyReadIndex) > 0); current.setLastReadUrl(aCursor.getInt(postIndexIndex)+""); current.setEditable(aCursor.getInt(editableIndex) == 1); + current.setHasModControls(aCursor.getInt(hasModControlsIndex) == 1); current.setIsOp(aCursor.getInt(isOpIndex) == 1); current.setIsPlat(aCursor.getInt(isPlatIndex) > 0); current.setRole(aCursor.getString(roleIndex)); @@ -496,6 +500,14 @@ public void setEditable(boolean aEditable) { mEditable = aEditable; } + public boolean hasModControls() { + return mHasModControls; + } + + public void setHasModControls(boolean aHasModControls) { + mHasModControls = aHasModControls; + } + /** diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulURL.java b/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulURL.java index ff29f64e8..18774275e 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulURL.java +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/thread/AwfulURL.java @@ -9,7 +9,7 @@ public class AwfulURL { - public enum TYPE{FORUM,THREAD,POST,EXTERNAL,NONE,INDEX,BANLIST} + public enum TYPE{FORUM,THREAD,POST,EXTERNAL,NONE,INDEX,BANLIST,MODQUEUE_REQUEST} private long id; private long pageNum = 1; private int perPage = Constants.ITEMS_PER_PAGE; @@ -17,6 +17,9 @@ public enum TYPE{FORUM,THREAD,POST,EXTERNAL,NONE,INDEX,BANLIST} private TYPE type = TYPE.NONE; private String gotoParam; private String fragment; + private long targetPostId; + private long threadId; + private boolean banRequest; public static AwfulURL forum(long id){ return forum(id, 1); @@ -121,6 +124,14 @@ public static AwfulURL parse(String url){ }else if(Constants.PATH_BANLIST.equals(uri.getLastPathSegment())){ aurl.type = TYPE.BANLIST; aurl.id = AwfulUtils.safeParseLong(uri.getQueryParameter(Constants.PARAM_USER_ID), 0); + }else if(Constants.PATH_MODQUEUE.equals(uri.getLastPathSegment()) + && (Constants.ACTION_REQUEST_PROBATION.equals(uri.getQueryParameter(Constants.PARAM_ACTION)) + || Constants.ACTION_REQUEST_BAN.equals(uri.getQueryParameter(Constants.PARAM_ACTION)))){ + aurl.type = TYPE.MODQUEUE_REQUEST; + aurl.banRequest = Constants.ACTION_REQUEST_BAN.equals(uri.getQueryParameter(Constants.PARAM_ACTION)); + aurl.id = AwfulUtils.safeParseLong(uri.getQueryParameter(Constants.PARAM_USER_ID), 0); + aurl.targetPostId = AwfulUtils.safeParseLong(uri.getQueryParameter(Constants.PARAM_TARGET_POST_ID), 0); + aurl.threadId = AwfulUtils.safeParseLong(uri.getQueryParameter(Constants.PARAM_THREAD_ID), 0); }else if("index.php".equalsIgnoreCase(uri.getLastPathSegment()) || uri.getPath() == null || uri.getPath().length() < 2){ aurl.type = TYPE.INDEX; }else{ @@ -172,6 +183,13 @@ public String getURL(int postPerPage){ url.appendQueryParameter(Constants.PARAM_PER_PAGE, Integer.toString(postPerPage)); url.appendQueryParameter(Constants.PARAM_POST_ID, Long.toString(id)); break; + case MODQUEUE_REQUEST: + url = Uri.parse(Constants.FUNCTION_MODQUEUE).buildUpon(); + url.appendQueryParameter(Constants.PARAM_ACTION, banRequest ? Constants.ACTION_REQUEST_BAN : Constants.ACTION_REQUEST_PROBATION); + url.appendQueryParameter(Constants.PARAM_USER_ID, Long.toString(id)); + url.appendQueryParameter(Constants.PARAM_TARGET_POST_ID, Long.toString(targetPostId)); + url.appendQueryParameter(Constants.PARAM_THREAD_ID, Long.toString(threadId)); + break; case EXTERNAL: return externalURL; case INDEX: @@ -250,6 +268,22 @@ public boolean isBanlist() { return type == TYPE.BANLIST; } + public boolean isModQueueRequest() { + return type == TYPE.MODQUEUE_REQUEST; + } + + public boolean isBanRequest() { + return banRequest; + } + + public long getTargetPostId() { + return targetPostId; + } + + public long getThreadId() { + return threadId; + } + public AwfulURL setPerPage(int postPerPage) { perPage = postPerPage; return this; diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/thread/ForumParsing.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/thread/ForumParsing.kt index 5aeee6ea0..5e1860014 100644 --- a/Awful.apk/src/main/java/com/ferg/awfulapp/thread/ForumParsing.kt +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/thread/ForumParsing.kt @@ -196,6 +196,7 @@ class PostParseTask( ?.let { put(EDITED, "${it.text()}") } put(EDITABLE, postData.getElementsByAttributeValue("alt", "Edit").isNotEmpty().sqlBool) + put(HAS_MOD_CONTROLS, postData.hasDescendantWithClass("modqueue_ctl").sqlBool) } } diff --git a/Awful.apk/src/main/java/com/ferg/awfulapp/users/ModQueueRequestFragment.kt b/Awful.apk/src/main/java/com/ferg/awfulapp/users/ModQueueRequestFragment.kt new file mode 100644 index 000000000..d33fcdc8b --- /dev/null +++ b/Awful.apk/src/main/java/com/ferg/awfulapp/users/ModQueueRequestFragment.kt @@ -0,0 +1,150 @@ +package com.ferg.awfulapp.users + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.Button +import android.widget.EditText +import android.widget.RadioButton +import android.widget.RadioGroup +import android.widget.Spinner +import android.widget.TextView +import com.android.volley.VolleyError +import com.ferg.awfulapp.AwfulFragment +import com.ferg.awfulapp.NavigationEvent +import com.ferg.awfulapp.NavigationEvent.Companion.parse +import com.ferg.awfulapp.R +import com.ferg.awfulapp.constants.Constants.PARAM_SUBMIT +import com.ferg.awfulapp.task.AwfulRequest +import com.ferg.awfulapp.task.ModQueueFormRequest +import com.ferg.awfulapp.task.ModQueueFormRequest.ModQueueForm +import com.ferg.awfulapp.task.ModQueueSubmitRequest +import com.ferg.awfulapp.util.bind + +/** + * Displays a modqueue probation/ban request page as a native form, and submits the request. + * + * The page to load is described by a [NavigationEvent.ModQueueRequest] parsed from the activity's + * intent. Submitting the request just waits for the form post to complete (without displaying the + * site's response) and closes the screen, so the user lands back where they came from. + */ +class ModQueueRequestFragment : AwfulFragment() { + + private val formContainer: View by bind(R.id.form_container) + private val userName: TextView by bind(R.id.user_name) + private val userHistory: TextView by bind(R.id.user_history) + private val durationSection: View by bind(R.id.duration_section) + private val durationSpinner: Spinner by bind(R.id.duration_spinner) + private val banTypeSection: View by bind(R.id.ban_type_section) + private val banTypeGroup: RadioGroup by bind(R.id.ban_type_group) + private val reasonInput: EditText by bind(R.id.reason_input) + private val notesInput: EditText by bind(R.id.notes_input) + private val submitButton: Button by bind(R.id.submit_button) + + private lateinit var request: NavigationEvent.ModQueueRequest + private var form: ModQueueForm? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = + inflateView(R.layout.modqueue_request, container, inflater) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + submitButton.setOnClickListener { submit() } + } + + override fun onActivityCreated(aSavedState: Bundle?) { + super.onActivityCreated(aSavedState) + val event = activity?.intent?.parse() as? NavigationEvent.ModQueueRequest + if (event == null) { + activity?.finish() + return + } + request = event + loadForm() + } + + override fun getTitle(): String? = + if (::request.isInitialized) { + getString(if (request.ban) R.string.mod_ban_request_title else R.string.mod_probation_request_title) + } else null + + private fun loadForm() { + activity?.let { context -> + queueRequest(ModQueueFormRequest(context, request.ban, request.userId, request.postId, request.threadId) + .build(this, object : AwfulRequest.AwfulResultCallback { + + override fun success(result: ModQueueForm) { + form = result + showForm(result) + } + + override fun failure(error: VolleyError?) {} + })) + } + } + + private fun showForm(form: ModQueueForm) { + userName.text = getString(R.string.mod_request_user, form.username ?: "") + userHistory.text = form.history + userHistory.visibility = if (form.history.isNullOrEmpty()) View.GONE else View.VISIBLE + + if (request.ban) { + banTypeSection.visibility = View.VISIBLE + banTypeGroup.removeAllViews() + form.banTypes.forEach { choice -> + val button = RadioButton(requireContext()).apply { + id = View.generateViewId() + text = choice.label + tag = choice.value + } + banTypeGroup.addView(button) + if (choice.selected) banTypeGroup.check(button.id) + } + } else { + durationSection.visibility = View.VISIBLE + durationSpinner.adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, form.durations.map { it.label }) + .apply { setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) } + durationSpinner.setSelection(form.durations.indexOfFirst { it.selected }.coerceAtLeast(0)) + } + formContainer.visibility = View.VISIBLE + } + + private fun submit() { + val form = form ?: return + val reason = reasonInput.text.toString().trim() + if (reason.isEmpty()) { + reasonInput.error = getString(R.string.mod_request_reason_required) + return + } + val choice = if (request.ban) { + val checked = banTypeGroup.findViewById(banTypeGroup.checkedRadioButtonId) ?: return + ModQueueFormRequest.FIELD_BAN_TYPE to checked.tag as String + } else { + val selected = form.durations.getOrNull(durationSpinner.selectedItemPosition) ?: return + ModQueueFormRequest.FIELD_DURATION to selected.value + } + val params = form.hiddenFields + choice + mapOf( + ModQueueFormRequest.FIELD_REASON to reason, + ModQueueFormRequest.FIELD_NOTES to notesInput.text.toString(), + PARAM_SUBMIT to "Submit" + ) + + submitButton.isEnabled = false + activity?.let { context -> + queueRequest(ModQueueSubmitRequest(context, params) + .build(this, object : AwfulRequest.AwfulResultCallback { + + override fun success(result: Void?) { + makeToast(if (request.ban) R.string.mod_request_ban_queued else R.string.mod_request_probation_queued) + activity?.finish() + } + + override fun failure(error: VolleyError?) { + submitButton.isEnabled = true + } + })) + } + } +} diff --git a/Awful.apk/src/main/res/drawable/ic_info_dark.xml b/Awful.apk/src/main/res/drawable/ic_info_dark.xml new file mode 100644 index 000000000..d531fd379 --- /dev/null +++ b/Awful.apk/src/main/res/drawable/ic_info_dark.xml @@ -0,0 +1,9 @@ + + + diff --git a/Awful.apk/src/main/res/layout/modqueue_request.xml b/Awful.apk/src/main/res/layout/modqueue_request.xml new file mode 100644 index 000000000..35db875d1 --- /dev/null +++ b/Awful.apk/src/main/res/layout/modqueue_request.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +