fix: make the browser Back button close a post instead of exiting the app
The URL was only ever synced with history.replaceState, so the app never created any history entries of its own -- Back always went straight to whatever page was open before this one, no matter where you were in the app. Opening a post now pushState's a new entry, matching how Instagram's own back button behaves: Back closes the post and returns to the grid. Closing a post any other way (the X button, the modal's own close handler) consumes that same entry via history.back() instead of piling a fresh one on top, and a popstate listener re-syncs app state for both directions. Tab switches and archive loads still use replaceState, unchanged -- only the post view gets its own step in history, deliberately, to keep the history stack shallow. Verified in a real browser: open a post, Back closes it and stays in the app; Forward reopens it; the X button closes it too, consuming the same entry rather than leaving a stale one behind. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011qAds5qr7nZRq5R4yAuxUk
This commit is contained in:
+56
-2
@@ -73,6 +73,17 @@ export default function App() {
|
|||||||
*/
|
*/
|
||||||
const initialRouteRef = useRef(parseRoute(window.location.pathname, window.location.search));
|
const initialRouteRef = useRef(parseRoute(window.location.pathname, window.location.search));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Back-button support for the post view. Every other URL change
|
||||||
|
* (`replaceState`s the tab/archive) is intentionally NOT pushed — only
|
||||||
|
* opening a post gets its own history entry, matching Instagram's own
|
||||||
|
* back-button behaviour: Back closes the post instead of leaving the app.
|
||||||
|
*/
|
||||||
|
const pushedPostRef = useRef(false);
|
||||||
|
/** Set while reacting to a popstate, so the URL-sync effect below does not
|
||||||
|
* try to push/replace/back() again for a change the browser already made. */
|
||||||
|
const suppressNextSyncRef = useRef(false);
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const profilePicInputRef = useRef<HTMLInputElement>(null);
|
const profilePicInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -367,6 +378,13 @@ export default function App() {
|
|||||||
// loader below is waiting to read.
|
// loader below is waiting to read.
|
||||||
if (!hasInitialLoaded) return;
|
if (!hasInitialLoaded) return;
|
||||||
|
|
||||||
|
// Consumed exactly once per popstate, regardless of what happens below:
|
||||||
|
// a popstate-driven change often already matches the URL (the browser
|
||||||
|
// already moved the pointer), which used to leave this flag stuck true
|
||||||
|
// and silently no-op the NEXT real close/open until something reset it.
|
||||||
|
const wasPopState = suppressNextSyncRef.current;
|
||||||
|
suppressNextSyncRef.current = false;
|
||||||
|
|
||||||
const archive = currentArchive?.name ?? (allPosts.length > 0 ? username : null) ?? null;
|
const archive = currentArchive?.name ?? (allPosts.length > 0 ? username : null) ?? null;
|
||||||
const nextPath = buildPath({
|
const nextPath = buildPath({
|
||||||
archive,
|
archive,
|
||||||
@@ -374,12 +392,48 @@ export default function App() {
|
|||||||
post: selectedPost ? postSlug(selectedPost) : null,
|
post: selectedPost ? postSlug(selectedPost) : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (nextPath !== window.location.pathname + window.location.search) {
|
if (nextPath === window.location.pathname + window.location.search) return;
|
||||||
console.log(`[Permalink] Updating URL to: ${nextPath}`);
|
if (wasPopState) return; // the browser already navigated; nothing to add
|
||||||
|
|
||||||
|
console.log(`[Permalink] Updating URL to: ${nextPath}`);
|
||||||
|
|
||||||
|
if (selectedPost && !pushedPostRef.current) {
|
||||||
|
// Opening a post: push, so Back closes it instead of leaving the app.
|
||||||
|
window.history.pushState(null, '', nextPath);
|
||||||
|
pushedPostRef.current = true;
|
||||||
|
} else if (!selectedPost && pushedPostRef.current) {
|
||||||
|
// Closing a post that was pushed for: consume that entry rather than
|
||||||
|
// piling a new one on top of it, so Back still means "one step".
|
||||||
|
pushedPostRef.current = false;
|
||||||
|
window.history.back();
|
||||||
|
} else {
|
||||||
window.history.replaceState(null, '', nextPath);
|
window.history.replaceState(null, '', nextPath);
|
||||||
}
|
}
|
||||||
}, [hasInitialLoaded, currentArchive?.name, username, allPosts.length, activeTab, selectedPost?.id]);
|
}, [hasInitialLoaded, currentArchive?.name, username, allPosts.length, activeTab, selectedPost?.id]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Back/forward support for the post view. Only a post push (above) ever
|
||||||
|
* creates an entry, so this only ever needs to open or close a post —
|
||||||
|
* never re-derive the tab or archive, which stayed on replaceState.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const onPopState = () => {
|
||||||
|
suppressNextSyncRef.current = true;
|
||||||
|
const route = parseRoute(window.location.pathname, window.location.search);
|
||||||
|
const post = route.post ? findPostBySlug(allPosts, route.post) : null;
|
||||||
|
if (post) {
|
||||||
|
setActiveTab(tabForSource(post.source));
|
||||||
|
setSelectedPost(post);
|
||||||
|
pushedPostRef.current = true; // forward navigation can land back here
|
||||||
|
} else {
|
||||||
|
setSelectedPost(null);
|
||||||
|
pushedPostRef.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('popstate', onPopState);
|
||||||
|
return () => window.removeEventListener('popstate', onPopState);
|
||||||
|
}, [allPosts]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasInitialLoaded) return;
|
if (hasInitialLoaded) return;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user