HellDots

Eventos y callbacks

La unión ChangeEvent, los diez callbacks específicos que la reflejan, y los cinco que reportan algo que no es un cambio.

Cada mutación está disponible de dos formas: como un flujo, o como un callback específico. Llevan exactamente los mismos eventos en exactamente los mismos momentos, con los mismos metadatos. Suscríbete a unos, a otros, o a los dos.

createCommentOverlay({
  onChange: (event) => api.post('/helldots-events', event),
});

Un manejador que lanza se captura y se avisa por consola. Nunca revierte la mutación que lo emitió.

ChangeEvent

Una unión discriminada por type. Haz switch sobre él y TypeScript estrecha el resto.

type ChangeEvent =
  | ({ type: 'comment:created'; comment: SerializedComment } & ChangeMeta)
  | ({ type: 'comment:edited'; comment: SerializedComment } & ChangeMeta)
  | ({ type: 'comment:deleted'; id: CommentId } & ChangeMeta)
  | ({ type: 'comment:status-changed'; comment: SerializedComment } & StatusChangeMeta)
  | ({ type: 'comment:updated'; comment: SerializedComment } & UpdateMeta)
  | ({ type: 'comment:anchor-lost'; comment: SerializedComment } & ChangeMeta)
  | ({ type: 'reply:added'; comment: SerializedComment; reply: CommentReply } & ChangeMeta)
  | ({ type: 'reply:edited'; comment: SerializedComment; reply: CommentReply } & ChangeMeta)
  | ({ type: 'reply:deleted'; comment: SerializedComment; reply: CommentReply } & ChangeMeta)
  | ({ type: 'reaction:toggled'; comment: SerializedComment; reply: CommentReply | null } & ChangeMeta);

Los metadatos van aplanados sobre el evento, en lugar de anidados bajo una clave metaevent.origin, event.from, event.to, event.field.

Los metadatos

meta.origin

Presente en todos los eventos.

ValorSignifica
"user"Alguien actuando dentro del widget — un marcador, el popover del hilo, la bandeja
"host"Tu propio código llamando a un método

La interfaz del widget acciona exactamente los mismos métodos públicos que tú, así que esto es lo único que distingue unos de otros. Existe para aplicaciones multiusuario: aplicar un cambio que llegó por un socket significa llamar al método que emite, así que sin esto cada integración tendría que envolver sus escrituras en una bandera.

onChange: (event) => {
  if (event.origin === 'host') return; // escritura nuestra, devuelta en eco
  socket.emit('helldots', event);
};

comment:anchor-lost siempre es "host", incluida la repetición que produce cada notifyNavigation() — así que la misma guarda las silencia también.

StatusChangeMeta

interface StatusChangeMeta extends ChangeMeta {
  from: CommentStatus;
  to: CommentStatus;
}

Los dos extremos del movimiento, para distinguir "reabierto" de "resuelto" sin comparar con una copia anterior.

UpdateMeta

type UpdateMeta = ChangeMeta &
  (
    | { field: 'type'; from: CommentType | null; to: CommentType | null }
    | { field: 'priority'; from: CommentPriority | null; to: CommentPriority | null }
    | { field: 'tags'; from: string[]; to: string[] }
  );

Discriminado por field, así que estrecharlo da from y to correctamente tipados para cada uno de los tres.

Los diez callbacks de cambio

Todos terminan con un argumento meta, así que un manejador existente que lo ignore sigue funcionando sin cambios.

CallbackSe dispara cuando
onCommentCreated(comment, meta)Se guarda un comentario nuevo
onCommentEdited(comment, meta)Se reescribe el texto de un comentario
onCommentDeleted(id, meta)Se elimina un comentario
onCommentStatusChanged(comment, meta)El estado se mueve por el ciclo de vida — meta es StatusChangeMeta
onCommentUpdated(comment, meta)Cambian tipo, prioridad o etiquetas — meta es UpdateMeta
onAnchorLost(comment, meta)Un comentario no se pudo volver a anclar
onReplyAdded(comment, reply, meta)Se añade una respuesta a cualquier comentario
onReplyEdited(comment, reply, meta)Se reescribe el texto de una respuesta
onReplyDeleted(comment, reply, meta)Se elimina una respuesta
onReactionToggled(comment, reply, meta)Se añade o quita una reacción — reply es null en la raíz

onCommentStatusChanged

onCommentStatusChanged: (comment, { from, to }) => {
  if (from === 'resolved') notify(`${comment.author} reabrió esto`);
};

onCommentUpdated

onCommentUpdated: (comment, meta) => {
  if (meta.field === 'priority' && meta.to === 'high') page(comment);
  if (meta.field === 'tags') reindex(comment.id, meta.to);
};

onAnchorLost

Se dispara por cada comentario que no se pudo volver a anclar — por loadComments, y de nuevo por cada notifyNavigation que aterrice donde su elemento no existe. Esas repeticiones siempre llevan origin: "host".

onAnchorLost: (comment, meta) => {
  if (meta.origin === 'host') return; // una repetición de una navegación
  analytics.track('helldots:anchor-lost', { page: comment.page });
};

Un salto en estos después de un despliegue significa que una refactorización movió los elementos sobre los que la gente venía comentando.

Los cinco que no son cambios

onReady

onReady?: (overlay: CommentOverlay) => void;

El widget ha montado y todos los métodos son seguros de usar. Recibe la instancia porque, cuando el documento ya está parseado, el montaje ocurre dentro del constructor — antes de que createCommentOverlay() haya devuelto nada que asignar. El sitio correcto para loadComments().

onError

onError?: (error: unknown, context: ErrorContext) => void;

Fallos que el widget sobrevive pero que de otro modo solo encontrarías en la consola. El aviso de consola se queda de todas formas.

contextQué pasó
"capture"Una captura no se renderizó; el comentario se guarda sin ella
"storage"No se pudo escribir en localStorage; la copia de este navegador diverge
"load"Un registro pasado a loadComments estaba mal formado y se descartó
"link"Un manejador de onCommentRequested lanzó o rechazó
"transform"Un manejador de transformScreenshot falló; se conservó la data URL
onError: (error, context) => {
  if (context === 'storage') toast.warn('No se pudieron guardar los comentarios localmente.');
  logger.warn({ context }, String(error));
};

onCommentRequested

onCommentRequested?: (id: CommentId) => void | Promise<unknown>;

Una URL de "Copiar enlace" apunta a un comentario que el widget no tiene — una vez por id, no una vez por intento. Devuelve una promesa y el enlace se reintenta cuando se resuelva. Mira enlaces directos.

onCommentModeChanged

onCommentModeChanged?: (active: boolean) => void;

El modo comentario se activó o desactivó, sea como sea que se accionó — el botón de la barra, el atajo de teclado, el estado vacío de la bandeja, o el apagado automático tras guardar un comentario.

El atajo es la razón de que esto exista: quien integra nunca ve esa pulsación, así que una aplicación que necesita apartarse mientras alguien elige un elemento — pausar un carrusel, desactivar su propio drag-and-drop, atenuar una capa — no tiene otra señal.

onCommentOpened

onCommentOpened?: (comment: SerializedComment) => void;

Alguien abrió el hilo completo de un comentario, desde su marcador o desde el detalle de la bandeja — los dos únicos sitios donde las respuestas se pueden leer. No se dispara cuando la bandeja simplemente se vuelve a renderizar.

Esto es sobre lo que se construye un contador de no leídos. HellDots no guarda ningún estado de lectura propio, porque de quién es ese "leído" depende de una identidad que solo tú puedes persistir.

onCommentOpened: (comment) => api.post(`/comments/${comment.id}/read`);

En esta página