OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web / web / static / draft.js

64 lines · javascript ref:
// Draft recovery — keep what you typed when a write fails.
//
// THE PROBLEM. A post goes through hpp over P2P, and that can fail: the daemon is restarting, the peer
// is unreachable, the record cannot be signed yet. When it did, the browser navigated to a redirect and
// the text was simply GONE. Losing someone's words because the network blinked is the worst small bug a
// client can have, and it is entirely avoidable.
//
// Deliberately dumb: the value is mirrored to localStorage as you type, restored on load, and cleared
// only once a post has actually SUCCEEDED. Nothing is scheduled, nothing is synced, and if scripting is
// off the composer behaves exactly as it did before.
const KEY = 'favela.draft'

function fields () {
  // The main composer and the reply box are the same shape; both are worth protecting, keyed apart so a
  // half-written reply does not reappear in the timeline composer.
  return [...document.querySelectorAll('form.composer textarea[name="text"], form.replyf textarea[name="text"]')]
}
const keyFor = (ta) => KEY + ':' + (ta.closest('form.replyf') ? 'reply:' + location.pathname : 'composer')

for (const ta of fields()) {
  const k = keyFor(ta)
  // Restore first — but never clobber something the server already put there (a quote preview, an edit).
  if (!ta.value) {
    let saved = null
    try { saved = localStorage.getItem(k) } catch {}
    if (saved) {
      ta.value = saved
      const note = document.createElement('p')
      note.className = 'draftnote'
      note.textContent = 'restored an unsent draft'
      const clear = document.createElement('button')
      clear.type = 'button'
      clear.className = 'devbtn'
      clear.textContent = 'discard'
      clear.addEventListener('click', () => {
        ta.value = ''
        try { localStorage.removeItem(k) } catch {}
        note.remove()
      })
      note.appendChild(document.createTextNode(' '))
      note.appendChild(clear)
      ta.insertAdjacentElement('beforebegin', note)
    }
  }
  ta.addEventListener('input', () => {
    try { if (ta.value) localStorage.setItem(k, ta.value); else localStorage.removeItem(k) } catch {}
  })
  // Clear ONLY on a real success. A plain form navigates away on submit, so we cannot see the outcome
  // here — instead the server sets ?posted=1 on the redirect, and that is the signal. Clearing on submit
  // would throw the text away in exactly the case this exists for: the write that failed.
  const form = ta.closest('form')
  if (form) form.addEventListener('submit', () => { try { sessionStorage.setItem(KEY + '.pending', k) } catch {} })
}

// On load: if we arrived from a successful post, drop the draft it came from.
try {
  const params = new URLSearchParams(location.search)
  const pending = sessionStorage.getItem(KEY + '.pending')
  if (params.get('posted') === '1' && pending) {
    localStorage.removeItem(pending)
    sessionStorage.removeItem(KEY + '.pending')
  }
} catch {}