virtual_triggers = (
  table_id,
  viewname,
  {
    participant_field,
    msg_relation,
    msgsender_field,
    msgview,
    msgform,
    participant_maxread_field,
  }
) => {
  if (!msg_relation) return [];
  const [msgtable_name, msgkey_to_room] = msg_relation.split(".");
  const msgtable = Table.findOne({ name: msgtable_name });
  if (!msgsender_field) return [];

  return [
    {
      when_trigger: "Insert",
      table_id: msgtable.id,
      run: async (row) => {
        const state = getState();
        if (row[msgsender_field]) return; // TODO how else to avoid double emit
        const v = await View.findOne({ name: msgview });

        const html = await v.run(
          { id: row.id },
          {
            req: {
              getLocale: () => state.getConfig("default_locale", "en"),
              user: { id: 0 },
              __: (s) => s,
            },
            res: {},
          }
        );
        const tenant = db.getTenantSchema();
        state.emitRoom(tenant, viewname, row[msgkey_to_room], {
          append: html,
          pls_ack_msg_id: row.id,
        });
      },
    },
  ];
}
@param {*} table_id
@param {string} viewname
@param {object} opts
@param {*} opts.participant_field
@param {string} opts.msg_relation,
@param {string} opts.msgsender_field,
@param {string} opts.msgview,
@param {*} opts.msgform,
@param {*} opts.participant_maxread_field,
@returns {object[]}