Skip to content
This repository was archived by the owner on Oct 28, 2026. It is now read-only.
Merged
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ All notable changes to Allthing are recorded here. The format follows [Keep a Ch

### Fixed

- Write buttons retain their dialog accessibility attributes during permission discovery and key approval. A follow confirmed on the first hand-off check preserves its new follower count when the reader was known not to follow beforehand.
- Leaving a page closes its popovers immediately, disposing nested previews as the kept feed detaches; Back does not restore fading panels while closing timers are delayed.
- Leaving Home reuses observed card heights when their exact current sizes still match, reducing layout reads before a post opens while preserving Back geometry and the existing fallback for late size changes or unsupported browsers.
- A hover card dismissed with Escape cannot reopen when the pointer re-enters during its closing animation.
- A data center with no default 24-hour Home copy, or an expired retained one, can use a fresh public copy shared by another data center, preserving its original age and current time bounds, instead of waiting for the full upstream build. Missing or invalid copies retain the normal fallback, ordinary background refreshes still build normally, and other ranges and private feeds are unchanged.
- CI rejects browser tests that pass only on retry, so a flaky run cannot appear as a clean release check.
Expand All @@ -33,6 +36,8 @@ All notable changes to Allthing are recorded here. The format follows [Keep a Ch

### Development

- Post-opening performance checks retain bounded, sanitized hover, resource, frame, long-task and animation-frame diagnostics for each attempt, including failures. The click-to-post measurement, prefetch wait, sample count and strict 100 ms limit are unchanged.

- Pin the current Miniflare development dependency's Undici to 7.30.1, fixing its [EventSource memory](https://github.com/advisories/GHSA-4qv8-2p9j-2685), [integrity-check](https://github.com/advisories/GHSA-f72v-6w45-q472) and [content-encoding](https://github.com/advisories/GHSA-xjj6-f33q-929c) advisories. The version-scoped override leaves Wrangler, Miniflare and workerd unchanged; Wrangler 4.152.0 also still pins an affected version.

- Smoothness checks retain bounded, sanitized raw input, long-task, animation-frame and frame diagnostics after each profile closes, including on failure. Metric inputs, strict checks and timing limits are unchanged.
Expand Down
3 changes: 3 additions & 0 deletions crates/allthing-web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ features = [
"Document",
"DomMatrixReadOnly",
"DomRect",
"DomRectReadOnly",
"DomTokenList",
"Element",
"EventTarget",
Expand Down Expand Up @@ -81,6 +82,8 @@ features = [
"ReadableStream",
"ReadableStreamDefaultReader",
"Response",
"ResizeObserver",
"ResizeObserverEntry",
"ScrollBehavior",
"ScrollIntoViewOptions",
"ScrollLogicalPosition",
Expand Down
4 changes: 4 additions & 0 deletions crates/allthing-web/src/components/cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ pub fn CastCard(
if let Some(element) = node.get() {
if track_seen {
crate::seen::watch(&element, seen);
crate::kept::watch_height(&element);
}
watch_for_prefetch(&element);
}
Expand All @@ -82,6 +83,9 @@ pub fn CastCard(
if let Some(element) = node.get_untracked() {
unwatch(&element);
crate::seen::unwatch(&element);
if track_seen {
crate::kept::unwatch_height(&element);
}
}
});
view! {
Expand Down
2 changes: 1 addition & 1 deletion crates/allthing-web/src/components/chips.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ fn acting_stat(
// hands off (what the reader did in the app shows); signed
// out, only a way to the app.
aria-pressed=move || (!hands_off.get() || shows_mine.get()).then(|| mine.get().to_string())
aria-haspopup=move || hands_off.get().then_some("dialog")
aria-haspopup=move || writer.opens_dialog().then_some("dialog")
aria-busy=move || busy.get().then_some("true")
on:click=move |_| writer.toggle(target, reaction, ask(mine.get_untracked()), shown.get_untracked())
>
Expand Down
7 changes: 6 additions & 1 deletion crates/allthing-web/src/components/popover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,12 @@ pub fn Popover(
Effect::new(move |seen: Option<String>| {
let now = pathname.get();
if seen.is_some_and(|seen| seen != now) {
close();
// The old page is leaving, so there is no closing animation to
// show. Dispose nested previews now, including their dwell
// timers and window listeners, as the kept page detaches.
cancel();
pressing.set_value(false);
state.set(State::Closed);
}
now
});
Expand Down
27 changes: 21 additions & 6 deletions crates/allthing-web/src/components/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,12 @@ impl Writer {
self.can.get() == Can::No
}

/// A write without an approved key opens a dialog, including while
/// permission discovery is pending and when approval is still needed.
pub fn opens_dialog(self) -> bool {
self.can.get() != Can::Yes
}

/// Whether what the reader liked and recasted shows: signed in, once
/// it's known whether Allthing writes for them (with a key, or in the
/// Farcaster app).
Expand Down Expand Up @@ -1250,6 +1256,12 @@ impl Writer {
cast.id = run;
}
let about = expect.about();
// A first read may land after the reader acts in the other app.
// A known opposite state before this hand-off still makes it new.
let follow_was_done = match &expect {
Expect::Follow { fid, on } => self.mine.with_untracked(|m| m.following(*fid).map(|was| was == *on)),
_ => None,
};
// A newer hand-off of the same, taken back, goes back to where the
// first began.
let earlier = UNDO.with(|u| u.borrow_mut().remove(&about)).map(|(_, undo)| undo);
Expand Down Expand Up @@ -1289,7 +1301,7 @@ impl Writer {
if let Some(undo) = earlier.or(undo) {
UNDO.with(|u| u.borrow_mut().insert(about.clone(), (run, undo)));
}
self.watch(expect, url, run);
self.watch(expect, url, run, follow_was_done);
(about, run)
}

Expand Down Expand Up @@ -1343,10 +1355,11 @@ impl Writer {
/// view. Once it's there, it shows as done, the dialog closes if it's
/// still open, and a notice says so. Found at once, before the reader
/// can have done anything, a like or a follow was so already, and the
/// page was behind (a like older than their latest). A newer hand-off of
/// page was behind (a like older than their latest), unless a follow
/// was known to be opposite before handing it off. A newer hand-off of
/// the same takes its place; signing out ends it; not done in time, what
/// shows is taken back.
fn watch(self, expect: Expect, url: String, run: u64) {
fn watch(self, expect: Expect, url: String, run: u64, follow_was_done: Option<bool>) {
let Some(reader) = self.fid() else { return };
let about = expect.about();
WATCHES.with(|w| w.borrow_mut().insert(about.clone(), run));
Expand All @@ -1373,7 +1386,9 @@ impl Writer {
&& current(&about)
{
UNDO.with(|u| u.borrow_mut().remove(&about));
let already = first && matches!(expect, Expect::Reaction { .. } | Expect::Follow { .. });
let already = first
&& follow_was_done != Some(false)
&& matches!(expect, Expect::Reaction { .. } | Expect::Follow { .. });
if already {
self.was_so(&expect, &about)
} else {
Expand Down Expand Up @@ -2687,7 +2702,7 @@ pub fn FollowButton(fid: u64, username: String) -> impl IntoView {
class:unknown=move || writer.reader().is_some() && known.get().is_none()
aria-label=label.clone()
aria-pressed=move || writer.reader().is_some().then(|| following().to_string())
aria-haspopup=move || writer.hands_off().then_some("dialog")
aria-haspopup=move || writer.opens_dialog().then_some("dialog")
aria-busy=move || writer.follow_busy(fid).then_some("true")
on:click=move |_| writer.toggle_follow(fid, Ask::follow(url.clone(), &username, following()))
>
Expand Down Expand Up @@ -2755,7 +2770,7 @@ pub fn DeleteButton(hash: String, author: u64, username: String, noun: &'static
class="reply-button delete-button"
disabled=busy
aria-busy=move || busy.get().then_some("true")
aria-haspopup=move || writer.hands_off().then_some("dialog")
aria-haspopup=move || writer.opens_dialog().then_some("dialog")
on:click=move |_| {
if writer.hands_off() {
writer.delete(hash.clone(), Ask::delete(url.clone(), noun));
Expand Down
Loading
Loading