From 8bbe129030a9b7b1a2eafd09863360223b222fe9 Mon Sep 17 00:00:00 2001 From: GPT on behalf of DHH <2741+dhh@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:55:08 +0200 Subject: [PATCH] Preserve active ping editors during sidebar refreshes --- .../controllers/rooms_list_controller.js | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/app/javascript/controllers/rooms_list_controller.js b/app/javascript/controllers/rooms_list_controller.js index 11f5e3a..4dff9a7 100644 --- a/app/javascript/controllers/rooms_list_controller.js +++ b/app/javascript/controllers/rooms_list_controller.js @@ -8,9 +8,12 @@ export default class extends Controller { #disconnected = true #connection = 0 + #discardDirectEditor = false #readAt = new Map() async connect() { + this.element.addEventListener("turbo:click", this.#directNavigation) + this.element.addEventListener("turbo:before-frame-render", this.#preserveDirectEditor) this.channel ??= await cable.subscribeTo({ channel: "UnreadRoomsChannel" }, { connected: this.#channelConnected.bind(this), disconnected: this.#channelDisconnected.bind(this), @@ -19,6 +22,9 @@ export default class extends Controller { } disconnect() { + this.element.removeEventListener("turbo:click", this.#directNavigation) + this.element.removeEventListener("turbo:before-frame-render", this.#preserveDirectEditor) + this.#discardDirectEditor = false ignoringBriefDisconnects(this.element, () => { this.#channelDisconnected() this.channel?.unsubscribe() @@ -43,6 +49,36 @@ export default class extends Controller { } } + // Preserve the live picker through background refreshes, but allow deliberate navigation. + #directNavigation = event => { + if (event.target.closest("a")?.dataset.turboFrame === this.element.id) { + this.#discardDirectEditor = true + } + } + + #preserveDirectEditor = event => { + if (event.target !== this.element) return + const discard = this.#discardDirectEditor + this.#discardDirectEditor = false + if (discard) return + + const current = this.element.querySelector("#direct_rooms_control") + const incoming = event.detail.newFrame.querySelector("#direct_rooms_control") + if (!current?.querySelector('[data-autocomplete-target="input"]') || !incoming) return + + // Turbo's Bardo moves the actual element, keeping input, selections and controllers. + const controls = [ current, incoming ].map(element => [ element, element.hasAttribute("data-turbo-permanent") ]) + for (const [ element ] of controls) element.setAttribute("data-turbo-permanent", "") + const render = event.detail.render + event.detail.render = (...args) => { + try { + return render(...args) + } finally { + for (const [ element, permanent ] of controls) element.toggleAttribute("data-turbo-permanent", permanent) + } + } + } + async #channelConnected() { if (this.#disconnected) { this.#disconnected = false