| Commit message (Collapse) | Author | Age |
| | |
|
| |
|
|
| |
Existing client state, stored in local storage, is migrated to new keys (that mention "conversation" instead of "channel" where appropriate) the first time the client loads.
|
| | |
|
| |
|
|
| |
This is a **breaking change** for essentially all clients. Thankfully, there's presently just the one, so we don't need to go to much effort to accommoate that; the client is modified in this commit to adapt, users can reload their client, and life will go on.
|
| |
|
|
| |
We use the same event processing glue that the client has for keeping up with live events, which means that a significant chunk of state management code goes away entirely.
|
| | |
|
| |
|
|
| |
The hidden `textarea` used to attach the form value to the DOM was being included in the ARIA accessibility tree, at least in testing (I didn't check in a browser). While we could suppress this iwth `aria-role="hidden"`, the WHATWG recommendation is to Not Do That, and to find another way to hide the element, instead. Marking the element as hidden accomplishes that goal, _and_ gets rid of a style rule.
|
| |\ |
|
| | | |
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
In ae93188f0f4f36086622636ba9ae4810cbd1f8c9, `remote.channels.all` became a flat array of channels, instead of a map, in order to simplify some of the reasoning around how state changes propagate. However, I neglected to remove all Map-shaped calls referring to it.
This lead to some pretty interesting behaviour:
* The client could not track unread state, because reconciling local state against the remote state would find no remote state, then throw away local state entirely as a result.
* The client would not actually update when a new channel appeared.
* The client would not actually update when a channel disappeared.
|
| |\| |
|
| | |
| |
| |
| | |
`session`.
|
| | | |
|
| | |
| |
| |
| |
| |
| | |
modification.
This also avoids using `placeholder` on elements where it's nonstandard, like `<div>`s.
|
| | |
| |
| |
| |
| | |
* Give the input `div` a marker to tell screen readers &c that it is a textbox.
* Ensure that it participates in tab order. (Zero is a sentinel value, see <https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/tabindex>.)
|
| | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | |
This is purely an aesthetic choice:
* The DOM `reset()` function can be used to clear the form, but can't be used to clear editable DIVs. Binding the editable div to a (hidden) form field allows `reset()` to clear both.
* We can find the target `form` element out of the event, but the API needed to do so differs between events dispatched to form controls and events dispatched to random DOM nodes. Using `closest('form')` works for both kinds of event target.
In practice, there is little need to make sure the message input form uses "normal" DOM APIs for functional reasons. Everything inside `MessageInput` is controllable through the component's script. This change isn't based on a functional need, but rather in the hopes that integrating with the DOM APIs makes it easier for _code we don't control_ - screen readers, password managers, saved-form support in browsers, &c - to integrate with Pilcrow. It is purely speculative.
(This also used to be necessary because Firefox didn't support `contenteditable="plaintext-only"`, but [support was added in March][ff-pto]
[ff-pto]: https://www.mozilla.org/en-US/firefox/136.0/releasenotes/#:~:text=The%20value%20plaintext%2Donly%20can%20now%20be%20specified%20for%20the%20contenteditable%20attribute%2C%20making%20the%20raw%20text%20of%20an%20element%20editable%20but%20without%20supporting%20rich%20text%20formatting.
|
| | |
| |
| |
| |
| | |
* Suppress input (including paste) while the input is disabled.
* Style the input to make it visible that it's not accepting input.
|
| | |
| |
| |
| | |
For whatever reason, `innerText` captures interior line breaks, while `textContent` does not. Wild. DOM APIs.
|
| |/ |
|
| | |
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
| |
There is a subtle race conditon in this code, which is likely not fixable without a protocol change:
* Ghost messages can disappear before their "real" message replacement shows up, if the client finishes sending (i.e., receives an HTTP response on the POST) before the server delivers the real message.
* Ghost messages can be duplicated briefly, if the client receives the real message before the client finishes sending.
Both happen in practice; we make no ordering guarantees between requests.
To aviod this, we'd to give clients a way to correlate pending sends with received messages. This would require fundamentally the same capabilities, like per-operation nonces, that preventing duplicate operations will require.
|
| |
|
|
|
|
| |
A `MessageRun` is a visual container with a specific layout - bordered, with a drop shadow, with a name badge on the top-left, which is either positioned to the left (`other-message`) or right (`own-message`). It is content-agnostic.
This facilitates putting things besides live messages inside of a message run. As a side effect, this gets rid of ActiveChannel; most of what it was doing makes more sense living in the channel view's `+page.svelte`.
|
| |
|
|
|
|
|
|
| |
This was actually two issues in one!
Issue 1: `isRetryable` did not consider whether we got a response or not. It assumed that the presence of a request in the error signaled that the error was definitely due to network issues, when in fact it's the presence of a request _and_ the absence of a response. That's my misreading of the Axios docs; the replacement `isRetryable` is more thorough.
Issue 2: operations in the outbox queue that fail with an exception stop the outbox drain process from making further progress, _and_ they stay in the queue. The outbox now dequeues jobs that throw an exception, and restarts itself if it terminates with a non-empty queue. The code that does this is _heinous_, but it seems to work well enough… Words I'm sure I won't come to regret.
|
| |
|
|
|
|
|
|
|
|
| |
A handful of operations are "synchronized" - that is, the server sends back information about them when the client asks to perform them, but notifies _all_ clients of completion through the event stream. As of this writing, these operations include sending and deleting messages, creating and deleting channels, and anything that creates new users.
We can use the outbox for most of these. I've opted _not_ to use the outbox for creating users, as that often takes place when the client is not connected to the event stream (and can't be) and so cannot discover that the operation has been completed after it is sent.
Outboxed tasks are objects, not closures, even though they behave in closure-like ways (`send()` carries out what amounts to "attempt this operation until it succeeds"). This is deliberate; I want the properties of incomplete tasks to be inspectable down the line, so that we can put them in the UI. If they're mere closures, we can't do that.
It is deliberate that `outbox.postToChannel` et al do not return promises. I contemplated it, but it will interact weirdly with outbox serialization, when we get to that. Rather than relying on a promise to determine when an operation has completed, clients of `Outbox` should monitor its properties. (We can add additional view properties to make that easier.)
|
| |
|
|
|
|
|
|
| |
directly from the UI.
This primarily serves to free up the message input immediately, so that the user can start drafting their next message right away. The wait while a message is being sent is actively disruptive when using Pilcrow on a server with noticable latency (hi.grimoire.ca has around 700ms), and this largely alleviates it.
Unsent messages can be lost if the client is closed or deactivated before they make it to the head of the queue.
|
| |\
| |
| |
| | |
Merged in spite of misgivings. This method will loop over the request until it completes, even if the user moves to a view where the response is no longer relevant.
|
| | | |
|
| | |
| |
| |
| |
| |
| | |
This is intended to transparently resume the session (using `boot` to start over) after more serious connection interruptions. It interacts with the heartbeat timeout: we let the browser try to reconnect through `EventSource` on its own for up to 30 seconds, before intervening, closing the event source, and starting attempts to call `boot`.
This covers both initial boot, which will now hang if the server is unavailable (sorry), and reconnection after an event timeout. No other operations are retried (particularly, sending a message is _not_ retried).
|
| | | |
|
| |/
|
|
|
|
|
|
| |
* `target="_blank"`: when Pilcrow is running in a browser, clicking a link should not replace Pilcrow with the target of the link. Pilcrow is "app-like" enough that opening links in a new tab _by default_, without user intervention, is likely more appropriate.
* `rel="noreferrer"`, which (A) stops most UAs from setting a referrer header when following those links, and (B) also implies `noopener`, preventing the link target from using `window.opener` from reaching back into Pilcrow's DOM.
I briefly experimented with DOMPurify's `RETURN_DOM_FRAGMENT` mode, which would have made the tests somewhat easier to write, but I wasn't able to find a good way to integrate the returned `DocumentFragment` objects with Svelte components, so HTML-as-strings it is. Sigh.
|
| |
|
|
|
|
|
|
| |
The changes introduced in the previous commit make it possible to detect lost connections and restart them, so do so. The process is pretty simple - a new remote state is spun up using `/api/boot`, swapped in for the existing state, and a `new EventSource` is started from that new remote state to consume events.
This can induce some anomalies. For example, messages that arrive on the server between the loss of one connection and the creation of the next one just "show up" in boot, without ever appearing in the event stream. (This is technically also true on client startup, but it's easier to expect in that situation.) This is something we'll need to consider when implementing things like notifications or unread flags, though the ones we have today, which are state-based, do work fine.
By design, this _does not_ retry either the `/api/boot` call or the new event source setup. Event sources will try to reconnect on their own, up to a point, so that's fine, but we need to build something more robust for `/api/boot`. I want to tackle that separately from detecting lost connections and reacting to them, but that does mean that this is not a complete solution to client reconnects.
|
| | |
|
| |\ |
|
| | | |
|
| |/ |
|
| |
|
|
|
|
|
|
|
|
|
| |
Sorry about the thousand-line omnibus change; this is functionally a rewrite of the client's state tracking, flavoured to resemble the existing code as far as is possible, rather than something that can be parted out and committed in pieces.
Highlights:
* No more `store.writeable()`s. All state is now tracked using state runs or derivatives. State is still largely structured the way it was, but several bits of nested state have been rewritten to ensure that their properties are reactive just as much as their containers are.
* State is no longer global. `(app)/+layout` manages a stateful session, created via its load hook and started/stopped via component mount and destroy events. The session also tracks an event source for the current state, and feeds events into the state, broadly along the same lines as the previous stores-based approach.
Together these two changes fix up several rough spots integrating state with Svelte, and allow for the possibility of multiple states. This is a major step towards restartable states, and thus towards better connection management, which will require the ability to "start over" once a connection is restored.
|
| | |
|
| |
|
|
| |
Only once on load, then once per new message.
|
| |\ |
|
| | | |
|
| | | |
|
| | |\ |
|
| | | |
| | |
| | |
| | | |
That no longer vitally pertains.
|
| | | |
| | |
| | |
| | | |
To facilitate PWA behaviour.
|
| | | |
| | |
| | |
| | |
| | |
| | |
| | |
| | | |
This is stored locally, and, while parallel to channel info, is not the
same as.
Eventually, this may hold info about moot/decayed channels, and grow
unbounded. That'll need to be addressed.
|
| | | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | | |
When we hit the boot endpoint, we get the server's view of things. If we
just setChannels with that, we overwrite all our locally-stored info in
on things like lastReadAt. So we need to merge data.
Eventually, this might militate for a `meta` key containing an object of
locally stored data, rather than having to handle each key specially.
|
| | |/
|/|
| |
| |
| |
| |
| |
| | |
Access to "global" (maybe "external?") state is now handled at the top level of the component hierarchy, in `+page.svelte`, `+layout.svelte`, and their associated scripts. State is otherwise passed down through props, and changes are passed up through callbacks.
This is - hopefully - groundwork for refactoring state management a bit. I wanted to move access to state out to a smaller number of places, so that I have fewer places to update to implement reconnect logic. My broader goal is to make it easier to refactor these kinds of external side effects, as well, though no such changes are in this branch.
This change also makes testing a mile easier, since tests can interact with props and callbacks instead of emulating the whole HTTP request stack and the Pilcrow API. This change removes do-very-little tests.
|
| | | |
|
| | | |
|