1
1
mirror of https://github.com/srid/ema.git synced 2024-11-29 09:25:14 +03:00

Smooth route click (#9)

* misc

* Ugly, but works!

* refactor server

* better names

* document the server logic

* Remove refreshPage, no longer used

* refactor ws client
This commit is contained in:
Sridhar Ratnakumar 2021-04-22 10:34:01 -04:00 committed by GitHub
parent 0f3f2b5932
commit 7c7df0cb63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 84 additions and 45 deletions

View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -xe set -xe
PROJECT=$(basename `pwd`) PROJECT=$(basename `pwd`)
tmux new-session -A -s $PROJECT bin/run exec tmux new-session -A -s $PROJECT bin/run

View File

@ -110,7 +110,7 @@ mainWith folder = do
render :: Diary -> Route -> LByteString render :: Diary -> Route -> LByteString
render diary r = do render diary r = do
Tailwind.layout (H.title "My Diary") $ Tailwind.layout (H.title pageTitle) $
H.div ! A.class_ "container mx-auto" $ do H.div ! A.class_ "container mx-auto" $ do
let heading = let heading =
H.header H.header
@ -125,7 +125,13 @@ render diary r = do
heading $ show day heading $ show day
routeElem Index "Back to Index" routeElem Index "Back to Index"
maybe "not found" renderOrg (Map.lookup day diary) maybe "not found" renderOrg (Map.lookup day diary)
H.footer ! A.class_ "mt-2 text-center border-t-2 text-gray-500" $ do
"Powered by "
H.a ! A.href "https://github.com/srid/ema" ! A.target "blank_" $ "Ema"
where where
pageTitle = case r of
Index -> "My Diary"
OnDay day -> show day <> " -- My Diary"
routeElem r' w = routeElem r' w =
H.a ! A.class_ "text-xl text-purple-500 hover:underline" ! routeHref r' $ w H.a ! A.class_ "text-xl text-purple-500 hover:underline" ! routeHref r' $ w
routeHref r' = routeHref r' =

View File

@ -4,6 +4,7 @@
-- | TODO: Refactor this module -- | TODO: Refactor this module
module Ema.Server where module Ema.Server where
import Control.Concurrent.Async (race)
import Control.Exception (try) import Control.Exception (try)
import Data.LVar (LVar) import Data.LVar (LVar)
import qualified Data.LVar as LVar import qualified Data.LVar as LVar
@ -33,19 +34,42 @@ runServerWithWebSocketHotReload port model render = do
WS.withPingThread conn 30 (pure ()) $ do WS.withPingThread conn 30 (pure ()) $ do
subId <- LVar.addListener model subId <- LVar.addListener model
let log s = putTextLn $ "[" <> show subId <> "] :: " <> s let log s = putTextLn $ "[" <> show subId <> "] :: " <> s
log "ws connected" log "ws:connected"
let loop = do let askClientForRoute = do
msg <- WS.receiveData conn msg :: Text <- WS.receiveData conn
let r :: route = pure $
msg msg
& pathInfoFromWsMsg & pathInfoFromWsMsg
& routeFromPathInfo & routeFromPathInfo
& fromMaybe (error "invalid route from ws") & fromMaybe (error "invalid route from ws")
log $ "Browser requests next HTML for: " <> show r loop = do
val <- LVar.listenNext model subId -- Notice that we @askClientForRoute@ in succession twice here.
WS.sendTextData conn $ routeHtml val r -- The first route will be the route the client intends to observe
log $ "Sent HTML for " <> show r -- for changes on. The second route, *if* it is sent, indicates
loop -- that the client wants to *switch* to that route. This proecess
-- repeats ad infinitum: i.e., the third route is for observing
-- changes, the fourth route is for switching to, and so on.
watchingRoute <- askClientForRoute
log $ "[Watch]: <~~ " <> show watchingRoute
-- Listen *until* either we get a new value, or the client requests
-- to switch to a new route.
race (LVar.listenNext model subId) askClientForRoute >>= \case
Left newHtml -> do
-- The page the user is currently viewing has changed. Send
-- the new HTML to them.
WS.sendTextData conn $ routeHtml newHtml watchingRoute
log $ "[Watch]: ~~> " <> show watchingRoute
loop
Right nextRoute -> do
-- The user clicked on a route link; send them the HTML for
-- that route this time, ignoring what we are watching
-- currently (we expect the user to initiate a watch route
-- request immediately following this).
log $ "[Switch]: <~~ " <> show nextRoute
html <- LVar.get model
WS.sendTextData conn $ routeHtml html nextRoute
log $ "[Switch]: ~~> " <> show nextRoute
loop
try loop >>= \case try loop >>= \case
Right () -> pure () Right () -> pure ()
Left (err :: ConnectionException) -> do Left (err :: ConnectionException) -> do
@ -77,6 +101,11 @@ wsClientShim =
encodeUtf8 encodeUtf8
[text| [text|
<script type="module"> <script type="module">
// Replace the DOM with a new raw HTML
//
// This function tries to trigger evaluation of <script> tags in the
// HTML, but for some reason it doesn't seem to work reliably.
// cf. the shims in Ema.Helper.Tailwind
// https://stackoverflow.com/a/47614491/55246 // https://stackoverflow.com/a/47614491/55246
function setInnerHtml(elm, html) { function setInnerHtml(elm, html) {
elm.innerHTML = html; elm.innerHTML = html;
@ -89,29 +118,6 @@ wsClientShim =
}); });
} }
// Unused, right now.
function refreshPage() {
// The setTimeout is necessary, otherwise reload will hang forever (at
// least on Brave browser)
//
// The delayedRefresh trick (5000 and 2000) is for cases when the
// server hasn't reloaded fast enough, but the browser hangs forever
// in reload refresh state.
//
// FIXME: This is not enough. Cancel and retry the reload, as it is
// normal to have longer sessions of ghcid in error state while the
// user fixes their code.
setTimeout(function() {
window.location.reload();
}, 5000);
setTimeout(function() {
window.location.reload();
}, 2000);
setTimeout(function() {
window.location.reload();
}, 100);
};
// Ema Status indicator // Ema Status indicator
const messages = { const messages = {
connected: "Connected", connected: "Connected",
@ -138,7 +144,7 @@ wsClientShim =
document.getElementById("ema-indicator").style.display = "none"; document.getElementById("ema-indicator").style.display = "none";
}; };
// WebSocket logic // WebSocket logic: watching for server changes & route switching
function init() { function init() {
console.log("ema: Opening ws conn"); console.log("ema: Opening ws conn");
window.connecting(); window.connecting();
@ -146,29 +152,56 @@ wsClientShim =
// Call this, then the server will send update *once*. Call again for // Call this, then the server will send update *once*. Call again for
// continous monitoring. // continous monitoring.
function watchRoute() { function watchCurrentRoute() {
console.log(`ema: Observing changes to $${document.location.pathname}`);
ws.send(document.location.pathname); ws.send(document.location.pathname);
}; };
function switchRoute(path) {
console.log(`ema: Switching to $${path}`);
ws.send(path);
}
ws.onopen = () => { ws.onopen = () => {
// window.connected(); // window.connected();
window.hideIndicator(); window.hideIndicator();
console.log("ema: Observing server for changes"); watchCurrentRoute();
watchRoute();
}; };
ws.onclose = () => { ws.onclose = () => {
console.log("ema: closed; reconnecting .."); console.log("ema: closed; reconnecting ..");
window.reloading(); window.reloading();
// Reconnect after 1s, which is typical time it takes for ghcid to reboot.
// Then, retry in another 1s. Ideally we need an exponential retry logic.
setTimeout(init, 1000); setTimeout(init, 1000);
// refreshPage();
}; };
ws.onmessage = evt => { ws.onmessage = evt => {
console.log("ema: Resetting HTML body") console.log("ema: ✍ Replacing DOM")
setInnerHtml(document.documentElement, evt.data); setInnerHtml(document.documentElement, evt.data);
watchRoute(); // Intercept route click events, and ask server for its HTML whilst
// managing history state.
document.body.addEventListener(`click`, e => {
const origin = e.target.closest("a");
if (origin) {
if (window.location.host === origin.host) {
document.body.classList.add("opacity-20");
window.history.pushState({}, "", origin.pathname);
switchRoute(origin.pathname);
e.preventDefault();
};
}
});
// Continue observing
watchCurrentRoute();
}; };
window.onbeforeunload = evt => { ws.close(); }; window.onbeforeunload = evt => { ws.close(); };
window.onpagehide = evt => { ws.close(); }; window.onpagehide = evt => { ws.close(); };
// When the user clicks the back button, resume watching the URL in
// the addressback, which has the effect of loading it immediately.
window.onpopstate = function(e) {
document.body.classList.add("opacity-20");
watchCurrentRoute();
};
}; };
window.onpageshow = init; window.onpageshow = init;