run = async (
  table_id,
  viewname,
  {
    participant_field,
    msg_relation,
    msgsender_field,
    msgview,
    msgform,
    participant_maxread_field,
  },
  state,
  { req, res },
  { getRowQuery, updateQuery, optionsQuery }
) => {
  const table = Table.findOne({ id: table_id });
  const fields = table.getFields();
  readState(state, fields);
  if (!state.id) return "Need room id";
  const appState = getState();
  const locale = req.getLocale();
  const role = req && req.user ? req.user.role_id : 100;
  const __ = (s) => appState.i18n.__({ phrase: s, locale }) || s;
  if (!msgview || !msgform || !msgsender_field || !msg_relation)
    throw new InvalidConfiguration(
      `View ${viewname} incorrectly configured: must supply Message views, Message sender and Participant fields`
    );

  const [msgtable_name, msgkey_to_room] = msg_relation.split(".");
  let partRow, part_table_name, canWrite;
  if (participant_field) {
    const [part_table_name1, part_key_to_room, part_user_field] =
      participant_field.split(".");
    part_table_name = part_table_name1;
    // check we participate
    partRow = await getRowQuery(
      state.id,
      part_table_name,
      part_user_field,
      part_key_to_room
    );
    if (!partRow) return "You are not a participant in this room";
    canWrite = true;
  } else {
    // check we have and write access
    const canRead = role <= table.min_role_read;
    if (!canRead) return "You do not have access to this room";

    canWrite = role <= table.min_role_write;
  }
  const v = await View.findOne({ name: msgview });
  const vresps = await v.runMany(
    { [msgkey_to_room]: state.id },
    { req, res, orderBy: "id", orderDesc: true, limit }
  );
  vresps.reverse();
  const n_retrieved = vresps.length;

  const msglist = vresps.map((r) => r.html).join("");
  const formview = await View.findOne({ name: msgform });
  if (!formview)
    throw new InvalidConfiguration("Message form view does not exist");
  const { columns, layout } = formview.configuration;
  const msgtable = Table.findOne({ name: msgtable_name });
  const min_read_id = Math.min.apply(
    Math,
    vresps.map((r) => r.row.id)
  );
  if (participant_maxread_field) {
    const [part_table_name1, part_key_to_room1, part_maxread_field] =
      participant_maxread_field.split(".");
    const max_read_id = Math.max.apply(
      Math,
      vresps.map((r) => r.row.id)
    );
    if (vresps.length > 0 && partRow && part_table_name)
      await updateQuery(
        partRow,
        part_table_name,
        max_read_id,
        part_maxread_field
      );
  }
  const form = await getForm(msgtable, viewname, columns, layout, null, req);

  form.class = `room-${state.id}`;
  form.hidden("room_id");
  form.values = { room_id: state.id };
  await form.fill_fkey_options(
    false,
    optionsQuery,
    req.user || { role_id: 100 }
  );
  await transformForm({
    form,
    table: msgtable,
    req,
    res,
    viewname: msgform,
  });
  return div(
    n_retrieved === limit &&
      button(
        {
          class: "btn btn-outline-secondary mb-1 fetch_older",
          onclick: `room_older('${viewname}',${state.id},this)`,
          "data-lt-msg-id": min_read_id,
        },
        req.__("Show older messages")
      ),
    div({ class: `msglist-${state.id}`, "data-user-id": req.user.id }, msglist),
    canWrite && renderForm(form, req.csrfToken()),
    script({
      src: `/static_assets/${db.connectObj.version_tag}/socket.io.min.js`,
    }) + script(domReady(`init_room("${viewname}", ${state.id})`))
  );
}
@param {string} table_id
@param {string} viewname
@param {object} optsOne
@param {string} optsOne.participant_field,
@param {string} optsOne.msg_relation
@param {*} optsOne.msgsender_field
@param {string} optsOne.msgview
@param {string} optsOne.msgform
@param {string} optsOne.participant_maxread_field
@param {object} state
@param {object} optsTwo
@param {object} optsTwo.req
@param {object} optsTwo.res
@returns {Promise<div>}
@throws {InvalidConfiguration}