Progress commit

- Slynk server
- Formatting output
- wait-for-clack-handler
- The WebSocket snippet from
  https://lispcookbook.github.io/cl-cookbook/websockets.html
  adatpted instead of hx-sse extension
- Persistent bottom navbar
- white-space: nowrap
This commit is contained in:
Bubblegumdrop 2024-10-13 00:31:11 -04:00
parent 8d3e31ef56
commit 0dbf9c90a6
5 changed files with 62 additions and 44 deletions

View File

@ -20,11 +20,12 @@
;; Start the chat server ;; Start the chat server
(defun start-chat-server (&rest args &key (address "localhost") (port 8080) &allow-other-keys) (defun start-chat-server (&rest args &key (address "localhost") (port 8080) &allow-other-keys)
"Start the Clack chat server." "Start the Clack chat server."
(slynk:create-server :port 4005 :dont-close t)
(setf *handler* (setf *handler*
(apply #'clack:clackup *app* (apply #'clack:clackup *app*
:port port :host address args)) :port port :host address args))
(when *handler* (when *handler*
(format t "Chat server started on ~A port ~A~%" address port))) (format t ";; Chat server started on ~A port ~A~%" address port)))
(defun stop-chat-server (&rest args) (defun stop-chat-server (&rest args)
(declare (ignore args)) (declare (ignore args))
@ -33,20 +34,22 @@
(clack:stop *handler*) (clack:stop *handler*)
(setf *handler* nil))) (setf *handler* nil)))
(unless *handler* (unless *handler*
(format t "Chat server stopped.~%"))) (format t ";; Chat server stopped.~%")))
(defun restart-chat-server (&rest args) (defun restart-chat-server (&rest args)
(apply #'stop-chat-server args) (apply #'stop-chat-server args)
(apply #'start-chat-server args)) (apply #'start-chat-server args))
(defun main (&rest args &key (foreground nil) &allow-other-keys) (defun main (&rest args &key (foreground t) &allow-other-keys)
(progn (setf (cl-who:html-mode) :html5)
(setf (cl-who:html-mode) :html5) (create-messages-table)
(create-messages-table) (apply #'start-chat-server args)
(apply #'restart-chat-server args))) (print (bt:all-threads))
(if foreground
(wait-for-clack-handler "clack-handler-")))
;; https://stackoverflow.com/a/30424968 ;; https://stackoverflow.com/a/30424968
(defun wait-for-hunchentoot-listener (name) (defun wait-for-clack-handler (name)
(bt:join-thread (bt:join-thread
(find-if (find-if
(lambda (th) (lambda (th)

View File

@ -9,7 +9,7 @@
(defun subpath-prefix (path) (defun subpath-prefix (path)
(format nil "~a~a" *default-prefix* path)) (format nil "~a~a" *default-prefix* path))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Render chat messages ;; Render chat messages
(defun render-chat-messages () (defun render-chat-messages ()
@ -20,7 +20,7 @@
(loop for msg in messages (loop for msg in messages
do (cl-who:htm do (cl-who:htm
(:div :class "box" (:div :class "box"
:style "overflow:auto;" :style "overflow: auto;"
(cl-who:str msg))))) (cl-who:str msg)))))
""))) "")))
@ -29,6 +29,7 @@
"Render the main chat page with HTMX integration." "Render the main chat page with HTMX integration."
(cl-who:with-html-output-to-string (*standard-output* nil :prologue t :indent t) (cl-who:with-html-output-to-string (*standard-output* nil :prologue t :indent t)
(:html :lang "en" (:html :lang "en"
:class "has-navbar-fixed-bottom"
(:head (:head
(:meta :charset "utf-8") (:meta :charset "utf-8")
(:meta :name "viewport" :content "width=device-width, initial-scale=1") (:meta :name "viewport" :content "width=device-width, initial-scale=1")
@ -42,51 +43,63 @@
:integrity "sha512-dQu3OKLMpRu85mW24LA1CUZG67BgLPR8Px3mcxmpdyijgl1UpCM1RtJoQP6h8UkufSnaHVRTUx98EQT9fcKohw==" :integrity "sha512-dQu3OKLMpRu85mW24LA1CUZG67BgLPR8Px3mcxmpdyijgl1UpCM1RtJoQP6h8UkufSnaHVRTUx98EQT9fcKohw=="
:crossorigin "anonymous" :crossorigin "anonymous"
:referrerpolicy "no-referrer") :referrerpolicy "no-referrer")
(:script :src "https://cdnjs.cloudflare.com/ajax/libs/htmx/2.0.3/ext/ws.min.js" ;; (:script :src "https://cdnjs.cloudflare.com/ajax/libs/htmx/2.0.3/ext/ws.min.js"
:integrity "sha512-1OIiXEswZd/etj60BUwFmyoi0OhrWdoYlzayJpSBivoMV0pQPIQr+vtAn3W3htsbWtLRU8DrBl0epdK4DQbj/w==" ;; :integrity "sha512-1OIiXEswZd/etj60BUwFmyoi0OhrWdoYlzayJpSBivoMV0pQPIQr+vtAn3W3htsbWtLRU8DrBl0epdK4DQbj/w=="
:crossorigin "anonymous" ;; :crossorigin "anonymous"
:referrerpolicy "no-referrer")) ;; :referrerpolicy "no-referrer")
)
(:body (:body
(:section :class "section" (:section :class "section"
(:div :class "container" (:div :class "container"
(:h1 :class "title" "Live Chat") ;; :hx-ext "ws"
(:div :class "field has-addons" ;; :ws-connect (subpath-prefix "/ws-chat-messages")
(:div :class "control is-expanded"
(:input :class "input is-expanded"
:id "chat-input"
:autocomplete "off"
:placeholder "Enter your message..."
:type "text" :name "message"))
(:div :class "control"
(:button :class "button is-link is-light"
:type "submit" "Send")))))
(:section :class "section"
(:div :class "container"
:hx-ext "ws"
:ws-connect (subpath-prefix "/ws-chat-messages")
(:h3 :class "title is-3" "Chat Messages")
(:div :class "container" (:div :class "container"
:id "chat-messages" :id "chat-messages"
:hx-get "/chat-messages" ;; :hx-get "/chat-messages"
(cl-who:str (render-chat-messages))))) (cl-who:str (render-chat-messages)))))
(:script " (:nav :role "navigation"
//////////////////////////////////////////////////////////////////////////////// :class "navbar is-fixed-bottom is-spaced has-shadow"
(:div :class "navbar-menu is-active"
:id "navbarBasicExample"
(:div :class "navbar-start")
(:div :class "container"
(:div :class "field has-addons"
(:div :class "control is-expanded"
(:input :class "input is-expanded"
:id "chat-input"
:autocomplete "off"
:placeholder "Enter your message..."
:type "text" :name "message"))
(:div :class "control"
(:button :class "button is-link is-light"
:type "submit" "Send"))))
(:div :class "navbar-end")))
(:script (cl-who:str (js-snippet (subpath-prefix "/ws-chat-messages"))))))))
function receivedMessage(msg) { (defun js-snippet (path)
document.querySelector('#chat-messages') (format nil
.insertAdjacentHTML('afterbegin', msg.data); "function receivedMessage(msg) {
document.querySelector('#chat-messages').insertAdjacentHTML('afterbegin', msg.data);
} }
const ws = new WebSocket('wss://' + window.location.host + '~a');
// TODO {{ server-name }}:{{ server-port }}
const ws = new WebSocket('ws://' + window.location.host + '/ws-chat-messages');
ws.addEventListener('message', receivedMessage); ws.addEventListener('message', receivedMessage);
////////////////////////////////////////////////////////////////////////////////
const inputField = document.getElementById('chat-input'); const inputField = document.getElementById('chat-input');
inputField.addEventListener('keyup', (evt) => { inputField.addEventListener('keyup', (evt) => {
if (evt.key === 'Enter') { if (evt.key === 'Enter') {
ws.send(evt.target.value); ws.send(evt.target.value);
evt.target.value = ''; evt.target.value = '';
} }
});"))))) });
// https://bulma.io/documentation/components/navbar/#navbar-menu
document.addEventListener('DOMContentLoaded', () => {
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Add a click event on each of them
$navbarBurgers.forEach( el => {
el.addEventListener('click', () => {
const target = el.dataset.target;
const $target = document.getElementById(target);
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
});
});
});" path))

View File

@ -17,6 +17,7 @@
(let ((message (let ((message
(cl-who:with-html-output-to-string (*standard-output*) (cl-who:with-html-output-to-string (*standard-output*)
(:div :class "box" (:div :class "box"
:style "overflow: auto; white-space: nowrap;"
(format t "~a: ~a" (gethash connection *connections*) message))))) (format t "~a: ~a" (gethash connection *connections*) message)))))
(loop :for con :being :the :hash-key :of *connections* :do (loop :for con :being :the :hash-key :of *connections* :do
(send con message)))) (send con message))))
@ -25,6 +26,7 @@
(let ((message (let ((message
(cl-who:with-html-output-to-string (*standard-output*) (cl-who:with-html-output-to-string (*standard-output*)
(:div :class "box" (:div :class "box"
:style "overflow: auto; white-space: nowrap;"
(format t "... ~a disconnected." (format t "... ~a disconnected."
(gethash connection *connections*)))))) (gethash connection *connections*))))))
(loop :for con :being :the :hash-key :of *connections* :do (loop :for con :being :the :hash-key :of *connections* :do

View File

@ -15,6 +15,7 @@
#:clack #:clack
#:lack #:lack
#:ningle #:ningle
#:slynk
;; WebSocket chat server backend ;; WebSocket chat server backend
#:websocket-driver) #:websocket-driver)
:components ((:file "package") :components ((:file "package")

View File

@ -63,4 +63,3 @@
#:live-chat-routes)) #:live-chat-routes))
(in-package #:live-chat) (in-package #:live-chat)