mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
Six state-changing compose routes checked ownership and nothing else, so any page the user visited could call off a send, discard a draft or add and remove attachments. They now require the shared 'ajax' token, and every fetch() caller in the compose controllers sends it in the X-CSRF-Token header (unschedule's caller already did). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
81 lines
2.7 KiB
JavaScript
81 lines
2.7 KiB
JavaScript
import { Controller } from "@hotwired/stimulus"
|
|
import { requestFailed } from "../../request_errors.js"
|
|
import { csrfToken } from "../../csrf.js"
|
|
|
|
/**
|
|
* The Undo button in the "Sending…" toast.
|
|
*
|
|
* Wired again, and for the default path. Send has two shapes now, chosen by
|
|
* User::SETTING_COMPOSE_SEND_FEEDBACK: "hold" keeps the composer open and makes
|
|
* its own Send pill the cancel, and "optimistic" — the default — closes the
|
|
* composer at once and moves the cancel here. The wait is what people notice;
|
|
* the undo is what they rarely need.
|
|
*
|
|
* This was unwired for one release while the held shape was tried, and the
|
|
* complaint that brought it back was the obvious one: eight seconds of a window
|
|
* that will not close is a long time when you have finished writing.
|
|
*
|
|
* `hideAfter` IS the cancel window rather than a toast duration. The button
|
|
* goes when the toast goes, so a toast that outlived the window would offer a
|
|
* cancel that silently fails, and one that died early would take away a cancel
|
|
* that still worked.
|
|
*/
|
|
|
|
export default class extends Controller {
|
|
static values = {
|
|
url: String,
|
|
hideAfter: Number,
|
|
}
|
|
|
|
connect() {
|
|
this.shownAt = Date.now()
|
|
this.armHide(this.hideAfterValue)
|
|
}
|
|
|
|
armHide(delay) {
|
|
this.hideTimer = setTimeout(() => {
|
|
this.element.style.transition = "opacity 0.4s"
|
|
this.element.style.opacity = "0"
|
|
this.element.style.pointerEvents = "none"
|
|
}, Math.max(0, delay))
|
|
}
|
|
|
|
disconnect() {
|
|
clearTimeout(this.hideTimer)
|
|
}
|
|
|
|
async abort() {
|
|
clearTimeout(this.hideTimer)
|
|
this.element.style.pointerEvents = "none"
|
|
|
|
let response = null
|
|
|
|
try {
|
|
response = await fetch(this.urlValue, {
|
|
method: "POST",
|
|
headers: { "X-Requested-With": "XMLHttpRequest", "X-CSRF-Token": csrfToken() },
|
|
})
|
|
} catch {
|
|
// Handled below with response still null.
|
|
}
|
|
|
|
// An undo that did not land means the mail may still go out, so that
|
|
// has to be said — and the button given back for whatever is left of the window, so a dropped
|
|
// connection can be retried rather than leaving a dead Undo.
|
|
if (requestFailed(response)) {
|
|
this.element.style.pointerEvents = ""
|
|
this.armHide(this.hideAfterValue - (Date.now() - this.shownAt))
|
|
|
|
return
|
|
}
|
|
|
|
const html = await response.text()
|
|
Turbo.renderStreamMessage(html)
|
|
|
|
// Dismiss the parent toast immediately
|
|
this.element.closest("[data-controller~='ui--toast']")
|
|
?.__stimulusController?.dismiss()
|
|
?? this.element.closest("[data-controller~='ui--toast']")?.remove()
|
|
}
|
|
}
|