Workflows: add mark as read/unread actions

Fixed: twpowertools:147
Change-Id: I6bb2363256cfd2a0ff3aafb4df71f24561576b27
diff --git a/src/contentScripts/communityConsole/workflows/actionRunners/readState.js b/src/contentScripts/communityConsole/workflows/actionRunners/readState.js
new file mode 100644
index 0000000..73d9a4f
--- /dev/null
+++ b/src/contentScripts/communityConsole/workflows/actionRunners/readState.js
@@ -0,0 +1,21 @@
+import {CCApi} from '../../../../common/api.js';
+import {getAuthUser} from '../../../../common/communityConsoleUtils.js';
+
+export default class ReadStateRunner {
+  execute(readState, thread) {
+    // Although this should in theory be the last message ID, it seems like
+    // setting 0 marks the entire thread as read anyways.
+    const lastMessageId = readState ? '0' : '-1';
+
+    return CCApi(
+        'SetUserReadStateBulk', {
+          // bulkItem:
+          1: [{
+            1: thread.forumId,
+            2: thread.threadId,
+            3: lastMessageId,
+          }],
+        },
+        /* authenticated = */ true, getAuthUser());
+  }
+}
diff --git a/src/contentScripts/communityConsole/workflows/models/thread.js b/src/contentScripts/communityConsole/workflows/models/thread.js
index 0f54316..96f69de 100644
--- a/src/contentScripts/communityConsole/workflows/models/thread.js
+++ b/src/contentScripts/communityConsole/workflows/models/thread.js
@@ -97,6 +97,10 @@
     return this._details?.['2']?.['9'];
   }
 
+  get lastMessageId() {
+    return this._details?.['2']?.['10'];
+  }
+
   get payload() {
     return this._details?.['2']?.['13'];
   }
diff --git a/src/contentScripts/communityConsole/workflows/runner.js b/src/contentScripts/communityConsole/workflows/runner.js
index d181d46..35c291c 100644
--- a/src/contentScripts/communityConsole/workflows/runner.js
+++ b/src/contentScripts/communityConsole/workflows/runner.js
@@ -2,6 +2,7 @@
 import * as pb from '../../../workflows/proto/main_pb.js';
 
 import CRRunner from './actionRunners/replyWithCR.js';
+import ReadStateRunner from './actionRunners/readState.js';
 import Thread from './models/thread.js';
 
 export default class WorkflowRunner {
@@ -16,6 +17,7 @@
 
     // Initialize action runners:
     this._CRRunner = new CRRunner();
+    this._ReadStateRunner = new ReadStateRunner();
   }
 
   start() {
@@ -57,6 +59,12 @@
         return this._CRRunner.execute(
             this._currentAction?.getReplyWithCrAction?.(), this._currentThread);
 
+      case pb.workflows.Action.ActionCase.MARK_AS_READ_ACTION:
+        return this._ReadStateRunner.execute(true, this._currentThread);
+
+      case pb.workflows.Action.ActionCase.MARK_AS_UNREAD_ACTION:
+        return this._ReadStateRunner.execute(false, this._currentThread);
+
       default:
         return Promise.reject(new Error('This action isn\'t supported yet.'));
     }
diff --git a/src/workflows/manager/components/ActionEditor.js b/src/workflows/manager/components/ActionEditor.js
index c4b3faf..9a57509 100644
--- a/src/workflows/manager/components/ActionEditor.js
+++ b/src/workflows/manager/components/ActionEditor.js
@@ -61,6 +61,10 @@
           </wf-action-reply-with-cr>
         `;
 
+      case pb.workflows.Action.ActionCase.MARK_AS_READ_ACTION:
+      case pb.workflows.Action.ActionCase.MARK_AS_UNREAD_ACTION:
+        return nothing;
+
       default:
         return html`<p>This action has not yet been implemented.</p>`;
     }
@@ -97,6 +101,10 @@
 
   checkValidity() {
     if (this.readOnly || !kSupportedActions.has(this._actionCase)) return true;
+
+    const s = this._specificActionEditor();
+    if (!s) return true;
+
     return this._specificActionEditor().checkValidity();
   }
 
@@ -167,6 +175,14 @@
         value = new pb.workflows.Action.ReportAction;
         this.action.setReportAction(value);
         break;
+      case pb.workflows.Action.ActionCase.MARK_AS_READ_ACTION:
+        value = new pb.workflows.Action.MarkAsReadAction;
+        this.action.setMarkAsReadAction(value);
+        break;
+      case pb.workflows.Action.ActionCase.MARK_AS_UNREAD_ACTION:
+        value = new pb.workflows.Action.MarkAsUnreadAction;
+        this.action.setMarkAsUnreadAction(value);
+        break;
       default:
         this.action.clearReplyAction();
         this.action.clearMoveAction();
@@ -178,6 +194,8 @@
         this.action.clearSubscribeAction();
         this.action.clearVoteAction();
         this.action.clearReportAction();
+        this.action.clearMarkAsReadAction();
+        this.action.clearMarkAsUnreadAction();
     }
 
     this.requestUpdate();
diff --git a/src/workflows/manager/shared/actions.js b/src/workflows/manager/shared/actions.js
index 4551f1f..fcf12e1 100644
--- a/src/workflows/manager/shared/actions.js
+++ b/src/workflows/manager/shared/actions.js
@@ -13,9 +13,11 @@
   17: 'Subscribe/unsubscribe to thread',
   18: 'Vote thread',
   19: 'Report thread',
+  20: 'Mark as read',
+  21: 'Mark as unread',
 };
 
-export const kSupportedActions = new Set([6]);
+export const kSupportedActions = new Set([6, 20, 21]);
 
 export const kActionStyles = css`
   .action {
diff --git a/src/workflows/proto/main.proto b/src/workflows/proto/main.proto
index 3431a63..660be5e 100644
--- a/src/workflows/proto/main.proto
+++ b/src/workflows/proto/main.proto
@@ -77,6 +77,10 @@
     ReportType report_type = 1;
   }
 
+  message MarkAsReadAction {}
+
+  message MarkAsUnreadAction {}
+
   oneof action {
     ReplyAction reply_action = 1;
     MoveAction move_action = 2;
@@ -88,6 +92,8 @@
     SubscribeAction subscribe_action = 17;
     VoteAction vote_action = 18;
     ReportAction report_action = 19;
+    MarkAsReadAction mark_as_read_action = 20;
+    MarkAsUnreadAction mark_as_unread_action = 21;
   }
 }
 
diff --git a/src/workflows/proto/main_pb.js b/src/workflows/proto/main_pb.js
index d44f49d..25f18ef 100644
--- a/src/workflows/proto/main_pb.js
+++ b/src/workflows/proto/main_pb.js
@@ -19,6 +19,8 @@
 goog.exportSymbol('workflows.Action.ActionCase', null, proto);
 goog.exportSymbol('workflows.Action.AttributeAction', null, proto);
 goog.exportSymbol('workflows.Action.AttributeAction.AttributeAction', null, proto);
+goog.exportSymbol('workflows.Action.MarkAsReadAction', null, proto);
+goog.exportSymbol('workflows.Action.MarkAsUnreadAction', null, proto);
 goog.exportSymbol('workflows.Action.MarkDuplicateAction', null, proto);
 goog.exportSymbol('workflows.Action.MoveAction', null, proto);
 goog.exportSymbol('workflows.Action.ReplyAction', null, proto);
@@ -294,6 +296,48 @@
  * @extends {jspb.Message}
  * @constructor
  */
+proto.workflows.Action.MarkAsReadAction = function(opt_data) {
+  jspb.Message.initialize(this, opt_data, 0, -1, null, null);
+};
+goog.inherits(proto.workflows.Action.MarkAsReadAction, jspb.Message);
+if (goog.DEBUG && !COMPILED) {
+  /**
+   * @public
+   * @override
+   */
+  proto.workflows.Action.MarkAsReadAction.displayName = 'proto.workflows.Action.MarkAsReadAction';
+}
+/**
+ * Generated by JsPbCodeGenerator.
+ * @param {Array=} opt_data Optional initial data array, typically from a
+ * server response, or constructed directly in Javascript. The array is used
+ * in place and becomes part of the constructed object. It is not cloned.
+ * If no data is provided, the constructed object will be empty, but still
+ * valid.
+ * @extends {jspb.Message}
+ * @constructor
+ */
+proto.workflows.Action.MarkAsUnreadAction = function(opt_data) {
+  jspb.Message.initialize(this, opt_data, 0, -1, null, null);
+};
+goog.inherits(proto.workflows.Action.MarkAsUnreadAction, jspb.Message);
+if (goog.DEBUG && !COMPILED) {
+  /**
+   * @public
+   * @override
+   */
+  proto.workflows.Action.MarkAsUnreadAction.displayName = 'proto.workflows.Action.MarkAsUnreadAction';
+}
+/**
+ * Generated by JsPbCodeGenerator.
+ * @param {Array=} opt_data Optional initial data array, typically from a
+ * server response, or constructed directly in Javascript. The array is used
+ * in place and becomes part of the constructed object. It is not cloned.
+ * If no data is provided, the constructed object will be empty, but still
+ * valid.
+ * @extends {jspb.Message}
+ * @constructor
+ */
 proto.workflows.Workflow = function(opt_data) {
   jspb.Message.initialize(this, opt_data, 0, -1, proto.workflows.Workflow.repeatedFields_, null);
 };
@@ -474,7 +518,7 @@
  * @private {!Array<!Array<number>>}
  * @const
  */
-proto.workflows.Action.oneofGroups_ = [[1,2,3,4,5,6,16,17,18,19]];
+proto.workflows.Action.oneofGroups_ = [[1,2,3,4,5,6,16,17,18,19,20,21]];
 
 /**
  * @enum {number}
@@ -490,7 +534,9 @@
   STAR_ACTION: 16,
   SUBSCRIBE_ACTION: 17,
   VOTE_ACTION: 18,
-  REPORT_ACTION: 19
+  REPORT_ACTION: 19,
+  MARK_AS_READ_ACTION: 20,
+  MARK_AS_UNREAD_ACTION: 21
 };
 
 /**
@@ -540,7 +586,9 @@
     starAction: (f = msg.getStarAction()) && proto.workflows.Action.StarAction.toObject(includeInstance, f),
     subscribeAction: (f = msg.getSubscribeAction()) && proto.workflows.Action.SubscribeAction.toObject(includeInstance, f),
     voteAction: (f = msg.getVoteAction()) && proto.workflows.Action.VoteAction.toObject(includeInstance, f),
-    reportAction: (f = msg.getReportAction()) && proto.workflows.Action.ReportAction.toObject(includeInstance, f)
+    reportAction: (f = msg.getReportAction()) && proto.workflows.Action.ReportAction.toObject(includeInstance, f),
+    markAsReadAction: (f = msg.getMarkAsReadAction()) && proto.workflows.Action.MarkAsReadAction.toObject(includeInstance, f),
+    markAsUnreadAction: (f = msg.getMarkAsUnreadAction()) && proto.workflows.Action.MarkAsUnreadAction.toObject(includeInstance, f)
   };
 
   if (includeInstance) {
@@ -627,6 +675,16 @@
       reader.readMessage(value,proto.workflows.Action.ReportAction.deserializeBinaryFromReader);
       msg.setReportAction(value);
       break;
+    case 20:
+      var value = new proto.workflows.Action.MarkAsReadAction;
+      reader.readMessage(value,proto.workflows.Action.MarkAsReadAction.deserializeBinaryFromReader);
+      msg.setMarkAsReadAction(value);
+      break;
+    case 21:
+      var value = new proto.workflows.Action.MarkAsUnreadAction;
+      reader.readMessage(value,proto.workflows.Action.MarkAsUnreadAction.deserializeBinaryFromReader);
+      msg.setMarkAsUnreadAction(value);
+      break;
     default:
       reader.skipField();
       break;
@@ -736,6 +794,22 @@
       proto.workflows.Action.ReportAction.serializeBinaryToWriter
     );
   }
+  f = message.getMarkAsReadAction();
+  if (f != null) {
+    writer.writeMessage(
+      20,
+      f,
+      proto.workflows.Action.MarkAsReadAction.serializeBinaryToWriter
+    );
+  }
+  f = message.getMarkAsUnreadAction();
+  if (f != null) {
+    writer.writeMessage(
+      21,
+      f,
+      proto.workflows.Action.MarkAsUnreadAction.serializeBinaryToWriter
+    );
+  }
 };
 
 
@@ -2280,6 +2354,208 @@
 };
 
 
+
+
+
+if (jspb.Message.GENERATE_TO_OBJECT) {
+/**
+ * Creates an object representation of this proto.
+ * Field names that are reserved in JavaScript and will be renamed to pb_name.
+ * Optional fields that are not set will be set to undefined.
+ * To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
+ * For the list of reserved names please see:
+ *     net/proto2/compiler/js/internal/generator.cc#kKeyword.
+ * @param {boolean=} opt_includeInstance Deprecated. whether to include the
+ *     JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @return {!Object}
+ */
+proto.workflows.Action.MarkAsReadAction.prototype.toObject = function(opt_includeInstance) {
+  return proto.workflows.Action.MarkAsReadAction.toObject(opt_includeInstance, this);
+};
+
+
+/**
+ * Static version of the {@see toObject} method.
+ * @param {boolean|undefined} includeInstance Deprecated. Whether to include
+ *     the JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @param {!proto.workflows.Action.MarkAsReadAction} msg The msg instance to transform.
+ * @return {!Object}
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.workflows.Action.MarkAsReadAction.toObject = function(includeInstance, msg) {
+  var f, obj = {
+
+  };
+
+  if (includeInstance) {
+    obj.$jspbMessageInstance = msg;
+  }
+  return obj;
+};
+}
+
+
+/**
+ * Deserializes binary data (in protobuf wire format).
+ * @param {jspb.ByteSource} bytes The bytes to deserialize.
+ * @return {!proto.workflows.Action.MarkAsReadAction}
+ */
+proto.workflows.Action.MarkAsReadAction.deserializeBinary = function(bytes) {
+  var reader = new jspb.BinaryReader(bytes);
+  var msg = new proto.workflows.Action.MarkAsReadAction;
+  return proto.workflows.Action.MarkAsReadAction.deserializeBinaryFromReader(msg, reader);
+};
+
+
+/**
+ * Deserializes binary data (in protobuf wire format) from the
+ * given reader into the given message object.
+ * @param {!proto.workflows.Action.MarkAsReadAction} msg The message object to deserialize into.
+ * @param {!jspb.BinaryReader} reader The BinaryReader to use.
+ * @return {!proto.workflows.Action.MarkAsReadAction}
+ */
+proto.workflows.Action.MarkAsReadAction.deserializeBinaryFromReader = function(msg, reader) {
+  while (reader.nextField()) {
+    if (reader.isEndGroup()) {
+      break;
+    }
+    var field = reader.getFieldNumber();
+    switch (field) {
+    default:
+      reader.skipField();
+      break;
+    }
+  }
+  return msg;
+};
+
+
+/**
+ * Serializes the message to binary data (in protobuf wire format).
+ * @return {!Uint8Array}
+ */
+proto.workflows.Action.MarkAsReadAction.prototype.serializeBinary = function() {
+  var writer = new jspb.BinaryWriter();
+  proto.workflows.Action.MarkAsReadAction.serializeBinaryToWriter(this, writer);
+  return writer.getResultBuffer();
+};
+
+
+/**
+ * Serializes the given message to binary data (in protobuf wire
+ * format), writing to the given BinaryWriter.
+ * @param {!proto.workflows.Action.MarkAsReadAction} message
+ * @param {!jspb.BinaryWriter} writer
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.workflows.Action.MarkAsReadAction.serializeBinaryToWriter = function(message, writer) {
+  var f = undefined;
+};
+
+
+
+
+
+if (jspb.Message.GENERATE_TO_OBJECT) {
+/**
+ * Creates an object representation of this proto.
+ * Field names that are reserved in JavaScript and will be renamed to pb_name.
+ * Optional fields that are not set will be set to undefined.
+ * To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
+ * For the list of reserved names please see:
+ *     net/proto2/compiler/js/internal/generator.cc#kKeyword.
+ * @param {boolean=} opt_includeInstance Deprecated. whether to include the
+ *     JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @return {!Object}
+ */
+proto.workflows.Action.MarkAsUnreadAction.prototype.toObject = function(opt_includeInstance) {
+  return proto.workflows.Action.MarkAsUnreadAction.toObject(opt_includeInstance, this);
+};
+
+
+/**
+ * Static version of the {@see toObject} method.
+ * @param {boolean|undefined} includeInstance Deprecated. Whether to include
+ *     the JSPB instance for transitional soy proto support:
+ *     http://goto/soy-param-migration
+ * @param {!proto.workflows.Action.MarkAsUnreadAction} msg The msg instance to transform.
+ * @return {!Object}
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.workflows.Action.MarkAsUnreadAction.toObject = function(includeInstance, msg) {
+  var f, obj = {
+
+  };
+
+  if (includeInstance) {
+    obj.$jspbMessageInstance = msg;
+  }
+  return obj;
+};
+}
+
+
+/**
+ * Deserializes binary data (in protobuf wire format).
+ * @param {jspb.ByteSource} bytes The bytes to deserialize.
+ * @return {!proto.workflows.Action.MarkAsUnreadAction}
+ */
+proto.workflows.Action.MarkAsUnreadAction.deserializeBinary = function(bytes) {
+  var reader = new jspb.BinaryReader(bytes);
+  var msg = new proto.workflows.Action.MarkAsUnreadAction;
+  return proto.workflows.Action.MarkAsUnreadAction.deserializeBinaryFromReader(msg, reader);
+};
+
+
+/**
+ * Deserializes binary data (in protobuf wire format) from the
+ * given reader into the given message object.
+ * @param {!proto.workflows.Action.MarkAsUnreadAction} msg The message object to deserialize into.
+ * @param {!jspb.BinaryReader} reader The BinaryReader to use.
+ * @return {!proto.workflows.Action.MarkAsUnreadAction}
+ */
+proto.workflows.Action.MarkAsUnreadAction.deserializeBinaryFromReader = function(msg, reader) {
+  while (reader.nextField()) {
+    if (reader.isEndGroup()) {
+      break;
+    }
+    var field = reader.getFieldNumber();
+    switch (field) {
+    default:
+      reader.skipField();
+      break;
+    }
+  }
+  return msg;
+};
+
+
+/**
+ * Serializes the message to binary data (in protobuf wire format).
+ * @return {!Uint8Array}
+ */
+proto.workflows.Action.MarkAsUnreadAction.prototype.serializeBinary = function() {
+  var writer = new jspb.BinaryWriter();
+  proto.workflows.Action.MarkAsUnreadAction.serializeBinaryToWriter(this, writer);
+  return writer.getResultBuffer();
+};
+
+
+/**
+ * Serializes the given message to binary data (in protobuf wire
+ * format), writing to the given BinaryWriter.
+ * @param {!proto.workflows.Action.MarkAsUnreadAction} message
+ * @param {!jspb.BinaryWriter} writer
+ * @suppress {unusedLocalVariables} f is only used for nested messages
+ */
+proto.workflows.Action.MarkAsUnreadAction.serializeBinaryToWriter = function(message, writer) {
+  var f = undefined;
+};
+
+
 /**
  * optional ReplyAction reply_action = 1;
  * @return {?proto.workflows.Action.ReplyAction}
@@ -2650,6 +2926,80 @@
 };
 
 
+/**
+ * optional MarkAsReadAction mark_as_read_action = 20;
+ * @return {?proto.workflows.Action.MarkAsReadAction}
+ */
+proto.workflows.Action.prototype.getMarkAsReadAction = function() {
+  return /** @type{?proto.workflows.Action.MarkAsReadAction} */ (
+    jspb.Message.getWrapperField(this, proto.workflows.Action.MarkAsReadAction, 20));
+};
+
+
+/**
+ * @param {?proto.workflows.Action.MarkAsReadAction|undefined} value
+ * @return {!proto.workflows.Action} returns this
+*/
+proto.workflows.Action.prototype.setMarkAsReadAction = function(value) {
+  return jspb.Message.setOneofWrapperField(this, 20, proto.workflows.Action.oneofGroups_[0], value);
+};
+
+
+/**
+ * Clears the message field making it undefined.
+ * @return {!proto.workflows.Action} returns this
+ */
+proto.workflows.Action.prototype.clearMarkAsReadAction = function() {
+  return this.setMarkAsReadAction(undefined);
+};
+
+
+/**
+ * Returns whether this field is set.
+ * @return {boolean}
+ */
+proto.workflows.Action.prototype.hasMarkAsReadAction = function() {
+  return jspb.Message.getField(this, 20) != null;
+};
+
+
+/**
+ * optional MarkAsUnreadAction mark_as_unread_action = 21;
+ * @return {?proto.workflows.Action.MarkAsUnreadAction}
+ */
+proto.workflows.Action.prototype.getMarkAsUnreadAction = function() {
+  return /** @type{?proto.workflows.Action.MarkAsUnreadAction} */ (
+    jspb.Message.getWrapperField(this, proto.workflows.Action.MarkAsUnreadAction, 21));
+};
+
+
+/**
+ * @param {?proto.workflows.Action.MarkAsUnreadAction|undefined} value
+ * @return {!proto.workflows.Action} returns this
+*/
+proto.workflows.Action.prototype.setMarkAsUnreadAction = function(value) {
+  return jspb.Message.setOneofWrapperField(this, 21, proto.workflows.Action.oneofGroups_[0], value);
+};
+
+
+/**
+ * Clears the message field making it undefined.
+ * @return {!proto.workflows.Action} returns this
+ */
+proto.workflows.Action.prototype.clearMarkAsUnreadAction = function() {
+  return this.setMarkAsUnreadAction(undefined);
+};
+
+
+/**
+ * Returns whether this field is set.
+ * @return {boolean}
+ */
+proto.workflows.Action.prototype.hasMarkAsUnreadAction = function() {
+  return jspb.Message.getField(this, 21) != null;
+};
+
+
 
 /**
  * List of repeated fields within this message type.