-
Notifications
You must be signed in to change notification settings - Fork 38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Suggested removal reasons: proof-of-concept and discussion #451
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this idea a lot, and appreciate the thought you've put into this!
I'm hesitant to completely rewrite the removal reasons module from scratch right now; I think the better way to clean it up will be to work with the existing code and revise it in chunks, so we can take time to review the individual pieces of this effort without getting lost or missing too many things. Diffs, even complicated ones, are always easier to review than entire files being rewritten from the ground up.
In my opinion, the ideal way to move forward with this feature is to break it up into two major parts. First, let's do a PR that revises the existing removal reasons code to break up the prompt and the actual action—the promptRemovalReason
and applyRemovalResult
functions, as you've called them—as well as any other opportunistic refactoring we can do there that will make things easier. Ideally this will introduce all the internal changes we'll need to implement the feature, but without actually implementing the feature. That way, we can determine at that point whether the new strategy introduces any regressions in existing behavior, which we can catch before introducing completely new logic. Once we've got that done and tested, we can do a second PR that uses the revised internals to implement the new feature. This should honestly be the easy part.
To respond to some of your actual code:
-
I'm not a huge fan of the naming you have for the
ChosenRemovalOptions
model, specifically that thesubmission
key contains actions for both submissions and comments being removed. It's not clear at a glance thatcomment
refers not to actions taken on the comment being removed, but to the comment being left in reply to the removed item. I think an easy way to clean this up (as well a simplify some of the processing logic) would be to implement this as a model without nested keys, instead using longer, descriptive property names. This would also let you use object destructuring to set defaults for each option in theapplyRemovalResult
definition, which could be a bit nicer to read. -
As far as storing the settings for this goes, I definitely agree that attaching unique IDs for each removal reason and referencing that from elsewhere is the way to go. This can be done without a schema update since adding additional properties to our existing models isn't a breaking change, though we probably want to add a migration step to setting retrieval that will add unambiguous IDs to existing settings and write the result back immediately if none are already present (e.g. on first run after updating).
-
Handling
<input>
et. al. is a bit tricky because it's not particularly well structured. This feature will add special considerations for Replace RR HTML parsing with replaced tokens #118 (which I'm honestly reconsidering the direction of anyway). The simplest way to handle this I can think of is to just not handle inputs in storage at all, and instead display a popup when clicking a suggested reason with inputs to choose which inputs are used, but that seems like a bit of a cop-out as far as the whole "one-click removal reasons" concept goes. Without any way to reliably attach identifiers to input elements in the removal reason markup, I think any solution for prefilling those fields from the sub settings window is going to be pretty brittle, so I don't have any strong preference for how that's handled in storage - maybe just an array of strings that are sequentially filled into the replaced elements? We may also want to add logic when saving removal reasons to check if the associated prefilled settings need to be updated, e.g. if the number of inputs in the template changes. -
The removal reasons UI needs a rewrite (Refactor removal reasons to use UI builders instead of custom popup #186), but I feel like it could be too messy to do a huge amount of UI refactoring in the same PR as these changes. That said, it seems like having a more modular interface for removal reasons would probably be helpful for implementing the settings UI for this feature. It might be smart to tackle that issue before anything else.
So here's a large PR that is partially a rewrite of the removal reasons module and partially a proof of concept of the removal reasons. It's quite some spaghetti, so I recommend you read this first before you dive into the code (as I'll be referencing specific snippets in here).
Essentially, I'm exploring a feature that allows one to set up "premade" responses (or suggested removal reasons as they are called in this PR) to posts flagged with a certain AutoModerator report reason (although this could be extended to reports by others too). I personally mod several subreddits with AutoModerator rules that exist to catch one specific rule violation and are almost always either "approve" or "remove with this specific reason". This feature exists for those cases and essentially should help moderators simply press one button to "confirm" that the AutoModerator rule ended up being correct.
Given that removal reasons can be fairly complex (a single removal message contains a header, footer, possibly several "reasons" and each reason can also contain
<input>
s and<select>
s), there's no ideal approach to creating these canned responses. We could just allow users to specify a text field, similar to macros, but ideally we connect it to the removal reasons system for ease of use.Two approaches for storing a canned/suggested removal reason exist that I think are feasible. They are:
<input>
s and<select>
s. Then, build the message string on removal by querying the content of the rules and substituting input values where necessary.{author}
and other tokens, then only perform the last substitution on actual removal.For this proof-of-concept, I've opted for the second approach. After some consideration however, I'm not entirely sure whether that is the best approach. Further on it will hopefully become clear why I have my doubts about this approach.
Let's discuss the actual proof-of-concept. First, I've created a new removal reasons module that performs essentially the same function as the current one. If you're testing this, you should probably disable the original removal reason module as they will quite definitely interfere with each other.
As part of this split/duplication, I've extracted some logic for fetching the (possibly cached) subreddit toolbox configuration and resolving the stored removal reasons (including tracking removal reasons that reference some other PR). They are
TBHelpers.getSubredditConfig
andTBHelpers.getRemovalReasonsStorage
. These functions are simply cleanups of the existing code in removalreasons and they are of high enough quality to be added to toolbox regardless of whether the rest of this PR ever lands in the main repo. I've also added an additional function,TBHelpers.getRemovalReasonsSettings
which converts the serialized removal reason settings into usable values (deserializing, adding defaults where needed). It also renames some in my opinion unclear names. This function is only used in my new (PoC) code, but I think that one should consider porting this over if you ever clean up removal reasons as it is significantly easier to work with and better represents the current legal values for removal reasons.The meat of the actual addition sits in
newremovalreasons.js
. Before discussing the actual code, I want to explain the architecture for this reworked/PoC version of removal reasons.We can roughly break up the removal reason process into the following distinct steps:
The current
removalreasons
module is essentially doing all of this in a single spaghetti filled function. Parameters are passed all over the place through the DOM, some button presses are handled by waiting for a click on the entire div, etc. For my new version, I set out to untangle some of that mess (where possible).The new module explicitly splits the steps as described above. This makes for more clear code, but additionally it allows us to present the removal reason dialog to the user without actually having something to remove. This allows us to compose suggested removal reasons but execute them later, as we will see later when discussing the config section of suggested removal reasons.
In particular, the meat of
newremovalreasons.js
sits in two functions:promptRemovalReason
andapplyRemovalResult
.promptRemovalReason
This is the function that performs step two in the steps described above. It shows a popup to the user, allowing them to compose a removal message, configure sending options, and confirm. I think a good way to get an intuitive idea of what the function does without looking through all the code is to consider its TypeScript signature:
The actual implementation of this function can be here. It should be noted though that it is largely copied from the original and cleaned where needed. I wouldn't go and do extended code review on it now, given that it is more the general functionality of this function that matters for the discussion in this PR (plus, I'm sure it has bugs 😄).
As you can see, invoking this function and waiting for the result gives you a fairly comprehensive object that represents exactly what actions to take. Crucially however, nothing in this result is uniquely attached to the actual thing to be removed. This means that we can store the result of
promptRemovalReason
and apply it later (and this is exactly what happens with suggested removal reasons).For an example result of invoking
promptRemovalReason
, expand this:Example promptRemovalReason response
promptRemovalReason
's partner in crime isapplyRemovalResult
. It takes information on the thing we're removing (retrivable usingTBCore.getApiThingInfo
) alongside anChosenRemovalOptions
instance (i.e. the output ofpromptRemovalReason
) and actually applies the removal actions such as creating a comment, flairing the post, etc.I actually recommend you take a quick peek at that function. async/await combined with the structure of the
ChosenRemovalOptions
object means that the function is almost trivial and does not have to deal with complexities like "just a removal comment" vs "just a removal dm" vs "both" vs "none".The rest of
newremovalreasons
largely mirrors the original, which includes setting up listeners for removal buttons, adding the post-removal "Add removal reason" button, etc. There is some additional code for actually adding the "Remove using suggested removal reason" button but we'll discuss that later.Let's take a step back from the code and consider the general design of this PR and the suggested removal reasons feature now that you have an idea as to how the new removal reasons module is structured.
In this proof-of-concept, the flow essentially works as follows:
promptRemovalReason
, which will present the exact same removal modal the user is used to. The result of the modal (the set of actions to take) is persisted.applyRemovalResult
is called with the persisted actions configured earlier.This video shows the entire process in action.
You can review the actual code that handles this process in other sections of the PR, but since this is more a proof of concept and not the actual final code I don't think it is too important for you to read that right now. If you do want to, here's some interesting sections:
TBHelpers.parseAutomodReasons
Now, I want to discuss why I dislike this current implementation architecture. There's a few things here that are/could be issues.
Possible large storage consumption. Storing the result of
promptRemovalReason
means that the entire markdown removal reason is stored in the toolbox wiki page. If you have quite a bit of suggested removal reasons, that means you also have quite a bit of storage and duplication here.Desync between removal reasons and suggested removal reasons is possible. Since there's nothing linking a suggested removal reason and the "original" removal reasons that were selected, suggested removal reasons can become stale when someone edits a removal reason but doesn't update all the suggested removal reasons that included said removal reason.
Subpar error handling. The original/current removalreasons implementation has error handling that provides feedback to the user about which step of the removal went wrong. If applying the flair errors, a nice message will be added to the modal and the user gets the option to retry. In the new implementation, once
promptRemovalReason
has returned, the modal is already destroyed. We can still handle errors and show a message, but since the modal is destroyed we have to force the user to either retry with the exact same options, or enter everything again.Possible performance concerns. The original/current implementation specifically caches the removal modal and only clears it if multiple things are removed on a single page (such as the modqueue). In the new architecture, every invocation of
promptRemovalReason
is unique by design and can therefore not reuse the same modal.Additionally, this PoC itself has some known issues:
No support for
filter
ed automod actions. Current implementation only looks at the reports that are currently shown.filter
ed things do not contain a reports element and are therefore not detected.Partial/missing support for new reddit. I've barely tested it works on old reddit.
Doesn't remove the thing until after the modal is completed. Should be as simple as programmatically pressing the "yes" button though.
Mediocre UI. Both in settings, and the "Use suggested removal option" button flow.
As I mentioned in the beginning, an alternative way to implement this is to instead store a list of selected removal reasons through some sort of unique ID, along the values of the
<input>
and<select>
fields. This approach fixes some of the flaws from the architecture used in this PoC, but it comes with some other flaws on its own (cannot skip the modal entirely without rewriting removal reasons, requires a potential schema change). It would however largely allow us to avoid touching the spaghetti that is the old removal reasons.I've been typing this for quite some time now, but I think this is everything we discussed in the Discord and that I've personally considered. Open to comments.
As a sidenote, if you ever consider embedding something like Vue or React for toolbox UIs, consider me on board. Creating "interactive" UIs with jQuery and direct DOM manipulation makes me want to die.