Compare commits
3 Commits
8d3e31ef56
...
ec06b36df0
Author | SHA1 | Date | |
---|---|---|---|
|
ec06b36df0 | ||
|
ff1106123f | ||
|
0dbf9c90a6 |
@ -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)
|
||||||
|
@ -8,18 +8,18 @@
|
|||||||
(setf (ningle:route *app* "/" :method :GET)
|
(setf (ningle:route *app* "/" :method :GET)
|
||||||
#'(lambda (params)
|
#'(lambda (params)
|
||||||
(declare (ignore params))
|
(declare (ignore params))
|
||||||
(live-chat-ui:render-chat-ui)))
|
(live-chat-ui:render-chat-ui))
|
||||||
|
|
||||||
;; Route for chat messages
|
;; Route for chat messages
|
||||||
(setf (ningle:route *app* "/chat-messages" :method :GET)
|
(ningle:route *app* "/chat-messages" :method :GET)
|
||||||
#'(lambda (params)
|
#'(lambda (params)
|
||||||
(declare (ignore params))
|
(declare (ignore params))
|
||||||
(live-chat-ui:render-chat-messages)))
|
(live-chat-ui:render-chat-messages))
|
||||||
|
|
||||||
;; Route for posting messages
|
;; Route for posting messages
|
||||||
(setf (ningle:route *app* "/post-message" :method :POST)
|
(ningle:route *app* "/post-message" :method :POST)
|
||||||
#'(lambda (params)
|
#'(lambda (params)
|
||||||
(let ((message (cdr (assoc "message" params :test 'equal))))
|
(let ((message (cdr (assoc "message" params :test 'equal))))
|
||||||
(when (and message (not (string= message "")))
|
(when (and message (not (string= message "")))
|
||||||
(handle-post-message message)))
|
(live-chat-ws:handle-post-message message)))
|
||||||
(live-chat-ui:render-chat-messages)))
|
(live-chat-ui:render-chat-messages)))
|
||||||
|
@ -9,84 +9,134 @@
|
|||||||
|
|
||||||
(defun subpath-prefix (path)
|
(defun subpath-prefix (path)
|
||||||
(format nil "~a~a" *default-prefix* path))
|
(format nil "~a~a" *default-prefix* path))
|
||||||
|
|
||||||
|
(defun static-asset-string (path)
|
||||||
|
(uiop:read-file-string
|
||||||
|
(merge-pathnames
|
||||||
|
path
|
||||||
|
(asdf:system-relative-pathname
|
||||||
|
:live-chat #P"static/"))))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Render chat messages
|
;; Render chat messages
|
||||||
|
(defun generate-html-message (content)
|
||||||
|
(cl-who:with-html-output-to-string (*standard-output*)
|
||||||
|
(:div :class "block"
|
||||||
|
:hx-swap-oob "afterbegin:#chat-messages"
|
||||||
|
(:div :class "box"
|
||||||
|
:style "overflow: auto; white-space: nowrap;"
|
||||||
|
(cl-who:str content)))))
|
||||||
|
|
||||||
(defun render-chat-messages ()
|
(defun render-chat-messages ()
|
||||||
"Render the list of chat messages as HTML."
|
"Render the list of chat messages as HTML."
|
||||||
(let ((messages (fetch-messages)))
|
(let ((messages (fetch-messages)))
|
||||||
(if messages
|
(cl-who:with-html-output-to-string (*standard-output* nil :indent nil)
|
||||||
(cl-who:with-html-output-to-string (*standard-output* nil :indent t)
|
(loop for msg in messages
|
||||||
(loop for msg in messages
|
collect (cl-who:str (generate-html-message msg))))))
|
||||||
do (cl-who:htm
|
|
||||||
(:div :class "box"
|
|
||||||
:style "overflow:auto;"
|
|
||||||
(cl-who:str msg)))))
|
|
||||||
"")))
|
|
||||||
|
|
||||||
;; Render chat UI
|
;; Render chat UI
|
||||||
(defun render-chat-ui ()
|
(defun render-chat-ui ()
|
||||||
"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 nil)
|
||||||
(: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")
|
||||||
|
;; (:meta :http-equiv "Content-Security-Policy"
|
||||||
|
;; :content "default-src *; style-src self unsafe-inline; script-src self unsafe-inline unsafe-eval https://phntsm.ddns.net")
|
||||||
(:title "Live Chat")
|
(:title "Live Chat")
|
||||||
(:link :rel "stylesheet"
|
(:style
|
||||||
:href "https://cdnjs.cloudflare.com/ajax/libs/bulma/1.0.2/css/bulma.min.css"
|
(cl-who:str (static-asset-string #P"bulma.min.css")))
|
||||||
:integrity "sha512-RpeJZX3aH5oZN3U3JhE7Sd+HG8XQsqmP3clIbu4G28p668yNsRNj3zMASKe1ATjl/W80wuEtCx2dFA8xaebG5w=="
|
;; (:link :rel "stylesheet"
|
||||||
:crossorigin "anonymous"
|
;; :href "https://cdnjs.cloudflare.com/ajax/libs/bulma/1.0.2/css/bulma.min.css"
|
||||||
:referrerpolicy "no-referrer")
|
;; :integrity "sha512-RpeJZX3aH5oZN3U3JhE7Sd+HG8XQsqmP3clIbu4G28p668yNsRNj3zMASKe1ATjl/W80wuEtCx2dFA8xaebG5w=="
|
||||||
(:script :src "https://cdnjs.cloudflare.com/ajax/libs/htmx/2.0.3/htmx.min.js"
|
;; :crossorigin "anonymous"
|
||||||
:integrity "sha512-dQu3OKLMpRu85mW24LA1CUZG67BgLPR8Px3mcxmpdyijgl1UpCM1RtJoQP6h8UkufSnaHVRTUx98EQT9fcKohw=="
|
;; :referrerpolicy "no-referrer")
|
||||||
:crossorigin "anonymous"
|
(:script
|
||||||
:referrerpolicy "no-referrer")
|
(cl-who:str (static-asset-string #P"htmx.min.js")))
|
||||||
(:script :src "https://cdnjs.cloudflare.com/ajax/libs/htmx/2.0.3/ext/ws.min.js"
|
;; :integrity "sha512-dQu3OKLMpRu85mW24LA1CUZG67BgLPR8Px3mcxmpdyijgl1UpCM1RtJoQP6h8UkufSnaHVRTUx98EQT9fcKohw"
|
||||||
:integrity "sha512-1OIiXEswZd/etj60BUwFmyoi0OhrWdoYlzayJpSBivoMV0pQPIQr+vtAn3W3htsbWtLRU8DrBl0epdK4DQbj/w=="
|
;; :src "https://cdnjs.cloudflare.com/ajax/libs/htmx/2.0.3/htmx.min.js"
|
||||||
:crossorigin "anonymous"
|
;; :crossorigin "anonymous"
|
||||||
:referrerpolicy "no-referrer"))
|
;; :referrerpolicy "no-referrer")
|
||||||
|
(:script
|
||||||
|
(cl-who:str (static-asset-string #P"ws.min.js"))))
|
||||||
|
;; :src "https://cdnjs.cloudflare.com/ajax/libs/htmx/2.0.3/ext/ws.min.js"
|
||||||
|
;; :integrity "sha512-1OIiXEswZd/etj60BUwFmyoi0OhrWdoYlzayJpSBivoMV0pQPIQr+vtAn3W3htsbWtLRU8DrBl0epdK4DQbj/w"
|
||||||
|
;; :crossorigin "anonymous"
|
||||||
|
;; :referrerpolicy "no-referrer"))
|
||||||
(:body
|
(:body
|
||||||
(:section :class "section"
|
(:section :class "section"
|
||||||
(:div :class "container"
|
(:div :class "container"
|
||||||
(:h1 :class "title" "Live Chat")
|
|
||||||
(: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")))))
|
|
||||||
(: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"
|
||||||
|
:aria-label "main navigation"
|
||||||
|
(:div :class "navbar-brand"
|
||||||
|
(:a :class "navbar-item"
|
||||||
|
:href "https://phntsm.ddns.net/"
|
||||||
|
(:img :src "/favicon.png"
|
||||||
|
:alt "icon"))
|
||||||
|
(:div :class "navbar-item"
|
||||||
|
(:div :class "columns is-mobile"
|
||||||
|
(:div :class "column"
|
||||||
|
(:button :class "button is-link"
|
||||||
|
:onclick "moreInfo();"
|
||||||
|
"More Info 1"))
|
||||||
|
(:div :class "column"
|
||||||
|
(:button :class "button is-link"
|
||||||
|
:onclick "moreInfo();"
|
||||||
|
"More Info 2"))
|
||||||
|
(:div :class "column"
|
||||||
|
(:button :class "button is-link"
|
||||||
|
:onclick "moreInfo();"
|
||||||
|
"More Info 3"))))
|
||||||
|
(:a :role "button"
|
||||||
|
:class "navbar-burger"
|
||||||
|
:aria-label "menu"
|
||||||
|
:aria-expanded "false"
|
||||||
|
:data-target "navbarBasicExample"
|
||||||
|
(:span :aria-hidden "true")
|
||||||
|
(:span :aria-hidden "true")
|
||||||
|
(:span :aria-hidden "true")
|
||||||
|
(:span :aria-hidden "true")))
|
||||||
|
(:div :class "navbar-menu is-active"
|
||||||
|
:id "navbarBasicExample"
|
||||||
|
(:div :class "navbar-start"
|
||||||
|
:style "display: none;")
|
||||||
|
(:div :class "container navbar-middle"
|
||||||
|
(:div :class "navbar-item is-expanded"
|
||||||
|
:hx-ext "ws"
|
||||||
|
:ws-connect (subpath-prefix "/ws-chat-messages")
|
||||||
|
(:form :class "form is-expanded"
|
||||||
|
:style "width:100%;"
|
||||||
|
:id "chat-form"
|
||||||
|
:name "chat-form"
|
||||||
|
:ws-send ""
|
||||||
|
;; :method :post
|
||||||
|
;; :action (subpath-prefix "/post-message")
|
||||||
|
;; :hx-post (subpath-prefix "/post-message")
|
||||||
|
;; :hx-target "#chat-messages"
|
||||||
|
(:div :class "field has-addons is-expanded"
|
||||||
|
(:div :class "control is-expanded"
|
||||||
|
(:input :class "input is-expanded"
|
||||||
|
:id "chat-input"
|
||||||
|
:form "chat-form"
|
||||||
|
:autocomplete "off"
|
||||||
|
:placeholder "Enter your message..."
|
||||||
|
:type "text" :name "message"))
|
||||||
|
(:div :class "control"
|
||||||
|
(:button :class "button is-link is-light"
|
||||||
|
:id "submit-button"
|
||||||
|
:form "chat-form"
|
||||||
|
:type "submit" "Send"))))))
|
||||||
|
|
||||||
|
(:div :class "navbar-end"
|
||||||
|
:style "display: none;")))
|
||||||
|
(:script (cl-who:str (js-snippet (subpath-prefix "/ws-chat-messages"))))))))
|
||||||
|
|
||||||
function receivedMessage(msg) {
|
(defun js-snippet (path)
|
||||||
document.querySelector('#chat-messages')
|
(format nil (static-asset-string #P"chat.js") path))
|
||||||
.insertAdjacentHTML('afterbegin', msg.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO {{ server-name }}:{{ server-port }}
|
|
||||||
const ws = new WebSocket('ws://' + window.location.host + '/ws-chat-messages');
|
|
||||||
ws.addEventListener('message', receivedMessage);
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
const inputField = document.getElementById('chat-input');
|
|
||||||
inputField.addEventListener('keyup', (evt) => {
|
|
||||||
if (evt.key === 'Enter') {
|
|
||||||
ws.send(evt.target.value);
|
|
||||||
evt.target.value = '';
|
|
||||||
}
|
|
||||||
});")))))
|
|
||||||
|
@ -4,30 +4,34 @@
|
|||||||
|
|
||||||
(defun handle-post-message (message)
|
(defun handle-post-message (message)
|
||||||
"Handle a new message being posted to the chat."
|
"Handle a new message being posted to the chat."
|
||||||
(let ((message (cl-who:escape-string message)))
|
(let* ((message (gethash "message" (com.inuoe.jzon:parse message)))
|
||||||
(format *standard-output* "Message received: ~a~%" message)
|
(message (string-trim '(#\Newline #\Return)
|
||||||
(live-chat-db:insert-message message)))
|
(cl-who:escape-string message))))
|
||||||
|
(when (and message
|
||||||
|
(> (length message) 0)
|
||||||
|
(not (string= "" message)))
|
||||||
|
(format *standard-output* "Message received: ~a~%" message)
|
||||||
|
(live-chat-db:insert-message message))))
|
||||||
|
|
||||||
(defun handle-new-connection (con)
|
(defun handle-new-connection (con)
|
||||||
(setf (gethash con *connections*)
|
(setf (gethash con *connections*)
|
||||||
(princ-to-string (gensym "USER-"))))
|
(princ-to-string (gensym "USER-"))))
|
||||||
|
|
||||||
(defun broadcast-to-room (connection message)
|
(defun broadcast-to-room (connection message)
|
||||||
(handle-post-message message)
|
(let* ((message (handle-post-message message))
|
||||||
(let ((message
|
(message (live-chat-ui:generate-html-message
|
||||||
(cl-who:with-html-output-to-string (*standard-output*)
|
(format nil "~a: ~a"
|
||||||
(:div :class "box"
|
(gethash connection *connections*)
|
||||||
(format t "~a: ~a" (gethash connection *connections*) message)))))
|
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))))
|
||||||
|
|
||||||
(defun handle-close-connection (connection)
|
(defun handle-close-connection (connection)
|
||||||
(let ((message
|
(let ((message
|
||||||
(cl-who:with-html-output-to-string (*standard-output*)
|
(live-chat-ui:generate-html-message
|
||||||
(:div :class "box"
|
(format nil "... ~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
|
|
||||||
(send con message))))
|
(send con message))))
|
||||||
|
|
||||||
(defun make-websocket-server (env)
|
(defun make-websocket-server (env)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
;;;; live-chat.asd
|
;;;; live-chat.asd
|
||||||
|
|
||||||
(asdf:defsystem #:live-chat
|
(asdf:defsystem #:live-chat
|
||||||
:description "Twitch.TV-like live chat on the web. Uses HTMX to poll for messages."
|
:description "Twitch.TV-like live chat on the web. Uses HTMX and WebSockets to poll for messages."
|
||||||
:author "Bubblegumdrop <staticsunn@gmail.com>"
|
:author "Bubblegumdrop <staticsunn@gmail.com>"
|
||||||
:license "WTFPL 2+"
|
:license "WTFPL 2+"
|
||||||
:version "0.1.0"
|
:version "0.1.0"
|
||||||
@ -13,14 +13,16 @@
|
|||||||
#:cl-dbi
|
#:cl-dbi
|
||||||
#:cl-who
|
#:cl-who
|
||||||
#:clack
|
#:clack
|
||||||
|
#:com.inuoe.jzon
|
||||||
#:lack
|
#:lack
|
||||||
#:ningle
|
#:ningle
|
||||||
|
#:slynk
|
||||||
;; WebSocket chat server backend
|
;; WebSocket chat server backend
|
||||||
#:websocket-driver)
|
#:websocket-driver)
|
||||||
:components ((:file "package")
|
:components ((:file "package")
|
||||||
(:file "live-chat-db")
|
(:file "live-chat-db")
|
||||||
(:file "live-chat-ui")
|
(:file "live-chat-ui")
|
||||||
(:file "live-chat-routes")
|
(:file "live-chat-routes")
|
||||||
(:file "live-chat-cgi")
|
;; (:file "live-chat-cgi")
|
||||||
(:file "live-chat-ws")
|
(:file "live-chat-ws")
|
||||||
(:file "live-chat-main")))
|
(:file "live-chat-main")))
|
||||||
|
31
package.lisp
31
package.lisp
@ -18,9 +18,9 @@
|
|||||||
#:insert-message
|
#:insert-message
|
||||||
#:fetch-messages)
|
#:fetch-messages)
|
||||||
(:export #:set-subpath-prefix
|
(:export #:set-subpath-prefix
|
||||||
|
#:generate-html-message
|
||||||
#:render-chat-messages
|
#:render-chat-messages
|
||||||
#:render-chat-ui
|
#:render-chat-ui
|
||||||
#:handle-post-message
|
|
||||||
#:*messages*))
|
#:*messages*))
|
||||||
|
|
||||||
(defpackage #:live-chat-cgi
|
(defpackage #:live-chat-cgi
|
||||||
@ -28,20 +28,10 @@
|
|||||||
(:import-from #:live-chat-ui)
|
(:import-from #:live-chat-ui)
|
||||||
(:export #:cgi-handler))
|
(:export #:cgi-handler))
|
||||||
|
|
||||||
(defpackage #:live-chat-routes
|
|
||||||
(:use #:cl)
|
|
||||||
(:import-from #:live-chat-db
|
|
||||||
#:insert-message)
|
|
||||||
(:import-from #:live-chat-ui
|
|
||||||
#:handle-post-message
|
|
||||||
#:render-chat-messages
|
|
||||||
#:render-chat-ui)
|
|
||||||
(:local-nicknames (#:cl-who #:cl-who)
|
|
||||||
(#:myway #:myway))
|
|
||||||
(:export #:app))
|
|
||||||
|
|
||||||
(defpackage #:live-chat-ws
|
(defpackage #:live-chat-ws
|
||||||
(:use #:cl)
|
(:use #:cl)
|
||||||
|
(:import-from #:live-chat-ui
|
||||||
|
#:generate-html-message)
|
||||||
(:import-from #:websocket-driver
|
(:import-from #:websocket-driver
|
||||||
#:make-client
|
#:make-client
|
||||||
#:make-server
|
#:make-server
|
||||||
@ -51,10 +41,24 @@
|
|||||||
#:close-connection)
|
#:close-connection)
|
||||||
(:export #:make-websocket-server
|
(:export #:make-websocket-server
|
||||||
#:handle-close-connection
|
#:handle-close-connection
|
||||||
|
#:handle-post-message
|
||||||
#:broadcast-to-room
|
#:broadcast-to-room
|
||||||
#:handle-new-connection
|
#:handle-new-connection
|
||||||
#:*connections*))
|
#:*connections*))
|
||||||
|
|
||||||
|
(defpackage #:live-chat-routes
|
||||||
|
(:use #:cl)
|
||||||
|
(:import-from #:live-chat-db
|
||||||
|
#:insert-message)
|
||||||
|
(:import-from #:live-chat-ws
|
||||||
|
#:handle-post-message)
|
||||||
|
(:import-from #:live-chat-ui
|
||||||
|
#:render-chat-messages
|
||||||
|
#:render-chat-ui)
|
||||||
|
(:local-nicknames (#:cl-who #:cl-who)
|
||||||
|
(#:myway #:myway))
|
||||||
|
(:export #:app))
|
||||||
|
|
||||||
(uiop:define-package #:live-chat
|
(uiop:define-package #:live-chat
|
||||||
(:use #:cl)
|
(:use #:cl)
|
||||||
(:import-from #:clack #:clackup)
|
(:import-from #:clack #:clackup)
|
||||||
@ -63,4 +67,3 @@
|
|||||||
#:live-chat-routes))
|
#:live-chat-routes))
|
||||||
|
|
||||||
(in-package #:live-chat)
|
(in-package #:live-chat)
|
||||||
|
|
||||||
|
3
static/bulma.min.css
vendored
Normal file
3
static/bulma.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
68
static/chat.js
Normal file
68
static/chat.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// https://bulma.io/documentation/components/navbar/#navbar-menu
|
||||||
|
function addBurgerMenu() {
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
function addWSSEventListener () {
|
||||||
|
const ws = new WebSocket('wss://' + window.location.host + '~a');
|
||||||
|
|
||||||
|
function receivedMessage(msg) {
|
||||||
|
document.getElementById('chat-messages')
|
||||||
|
.insertAdjacentHTML('afterbegin', msg.data);
|
||||||
|
// document.querySelector('#chat-messages').insertAdjacentHTML('afterbegin', msg.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.addEventListener ('message', receivedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addInputFieldEventListener () {
|
||||||
|
const inputField = document.getElementById('chat-input');
|
||||||
|
inputField.addEventListener ('keyup', (evt) => {
|
||||||
|
if (evt.key === 'Enter') {
|
||||||
|
// clear the input on Enter
|
||||||
|
evt.target.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
function moreInfo() {
|
||||||
|
const chatForm = document.getElementById('chat-form');
|
||||||
|
const inputField = document.getElementById('chat-input');
|
||||||
|
const sendButton = document.getElementById('submit-button');
|
||||||
|
chatForm.reset ();
|
||||||
|
inputField.focus ();
|
||||||
|
document.execCommand('insertText', false, 'More Info!');
|
||||||
|
sendButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removing the form attribute "disables" the form allowing the
|
||||||
|
// websocket chat to be used instead.
|
||||||
|
function disableHTMLForm () {
|
||||||
|
const inputField = document.getElementById('chat-input');
|
||||||
|
const sendButton = document.getElementById('submit-button');
|
||||||
|
inputField.removeAttribute ('form');
|
||||||
|
sendButton.removeAttribute ('form');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
addBurgerMenu();
|
||||||
|
if (WebSocket) {
|
||||||
|
// Use websockets
|
||||||
|
addInputFieldEventListener();
|
||||||
|
// disableHTMLForm();
|
||||||
|
// addWSSEventListener();
|
||||||
|
}
|
||||||
|
// Fallback will be used otherwise.
|
||||||
|
});
|
1
static/htmx.min.js
vendored
Normal file
1
static/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/ws.min.js
vendored
Normal file
1
static/ws.min.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
!function(){var u;function s(e){return null!=u.getInternalData(e).webSocket}function h(e){return!u.bodyContains(e)&&(u.getInternalData(e).webSocket.close(),1)}function t(e){e=new WebSocket(e,[]);return e.binaryType=htmx.config.wsBinaryType,e}function o(e,t){var n=[];return(u.hasAttribute(e,t)||u.hasAttribute(e,"hx-ws"))&&n.push(e),e.querySelectorAll("["+t+"], [data-"+t+"], [data-hx-ws], [hx-ws]").forEach(function(e){n.push(e)}),n}function i(e,t){if(e)for(var n=0;n<e.length;n++)t(e[n])}htmx.version&&!htmx.version.startsWith("1.")&&console.warn("WARNING: You are using an htmx 1 extension with htmx "+htmx.version+". It is recommended that you move to the version of this extension found on https://htmx.org/extensions"),htmx.defineExtension("ws",{init:function(e){u=e,htmx.createWebSocket||(htmx.createWebSocket=t),htmx.config.wsReconnectDelay||(htmx.config.wsReconnectDelay="full-jitter")},onEvent:function(e,t){var n=t.target||t.detail.elt;switch(e){case"htmx:beforeCleanupElement":var r=u.getInternalData(n);return void(r.webSocket&&r.webSocket.close());case"htmx:beforeProcessNode":i(o(n,"ws-connect"),function(e){var o=e;if(u.bodyContains(o)){var t=u.getAttributeValue(o,"ws-connect");if(null==t||""===t){var n=function(e){e=u.getAttributeValue(e,"hx-ws");if(e)for(var t=e.trim().split(/\s+/),n=0;n<t.length;n++){var r=t[n].split(/:(.+)/);if("connect"===r[0])return r[1]}}(o);if(null==n)return;t=n}0===t.indexOf("/")&&(n=location.hostname+(location.port?":"+location.port:""),"https:"===location.protocol?t="wss://"+n+t:"http:"===location.protocol&&(t="ws://"+n+t));var i=function(r,t){var s={socket:null,messageQueue:[],retryCount:0,events:{},addEventListener:function(e,t){this.socket&&this.socket.addEventListener(e,t),this.events[e]||(this.events[e]=[]),this.events[e].push(t)},sendImmediately:function(e,t){this.socket||u.triggerErrorEvent(),t&&!u.triggerEvent(t,"htmx:wsBeforeSend",{message:e,socketWrapper:this.publicInterface})||(this.socket.send(e),t&&u.triggerEvent(t,"htmx:wsAfterSend",{message:e,socketWrapper:this.publicInterface}))},send:function(e,t){this.socket.readyState!==this.socket.OPEN?this.messageQueue.push({message:e,sendElt:t}):this.sendImmediately(e,t)},handleQueuedMessages:function(){for(;0<this.messageQueue.length;){var e=this.messageQueue[0];if(this.socket.readyState!==this.socket.OPEN)break;this.sendImmediately(e.message,e.sendElt),this.messageQueue.shift()}},init:function(){this.socket&&this.socket.readyState===this.socket.OPEN&&this.socket.close();var n=t(),e=(u.triggerEvent(r,"htmx:wsConnecting",{event:{type:"connecting"}}),(this.socket=n).onopen=function(e){s.retryCount=0,u.triggerEvent(r,"htmx:wsOpen",{event:e,socketWrapper:s.publicInterface}),s.handleQueuedMessages()},n.onclose=function(e){var t;!h(r)&&0<=[1006,1012,1013].indexOf(e.code)&&(t=function(e){var t=htmx.config.wsReconnectDelay;if("function"==typeof t)return t(e);if("full-jitter"===t)return t=Math.min(e,6),1e3*Math.pow(2,t)*Math.random();logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}(s.retryCount),setTimeout(function(){s.retryCount+=1,s.init()},t)),u.triggerEvent(r,"htmx:wsClose",{event:e,socketWrapper:s.publicInterface})},n.onerror=function(e){u.triggerErrorEvent(r,"htmx:wsError",{error:e,socketWrapper:s}),h(r)},this.events);Object.keys(e).forEach(function(t){e[t].forEach(function(e){n.addEventListener(t,e)})})},close:function(){this.socket.close()}};return s.init(),s.publicInterface={send:s.send.bind(s),sendImmediately:s.sendImmediately.bind(s),queue:s.messageQueue},s}(o,function(){return htmx.createWebSocket(t)});i.addEventListener("message",function(e){if(!h(o)){var t=e.data;if(u.triggerEvent(o,"htmx:wsBeforeMessage",{message:t,socketWrapper:i.publicInterface})){u.withExtensions(o,function(e){t=e.transformResponse(t,null,o)});var n=u.makeSettleInfo(o),e=u.makeFragment(t);if(e.children.length)for(var r=Array.from(e.children),s=0;s<r.length;s++)u.oobSwap(u.getAttributeValue(r[s],"hx-swap-oob")||"true",r[s],n);u.settleImmediately(n.tasks),u.triggerEvent(o,"htmx:wsAfterMessage",{message:t,socketWrapper:i.publicInterface})}}}),u.getInternalData(o).webSocket=i}}),i(o(n,"ws-send"),function(e){var a,c,t,n;e=e,(n=u.getAttributeValue(e,"hx-ws"))&&"send"!==n||(a=u.getClosestMatch(e,s),c=e,t=u.getInternalData(c),u.getTriggerSpecs(c).forEach(function(e){u.addTriggerHandler(c,e,t,function(e,t){var n,r,s,o,i;h(a)||(n=u.getInternalData(a).webSocket,r=u.getHeaders(c,u.getTarget(c)),i=(o=u.getInputValues(c,"post")).errors,o=o.values,s=u.getExpressionVars(c),o=u.mergeObjects(o,s),s={parameters:u.filterValues(o,c),unfilteredParameters:o,headers:r,errors:i,triggeringEvent:t,messageBody:void 0,socketWrapper:n.publicInterface},u.triggerEvent(e,"htmx:wsConfigSend",s)&&(i&&0<i.length?u.triggerEvent(e,"htmx:validation:halted",i):(void 0===(o=s.messageBody)&&(i=Object.assign({},s.parameters),s.headers&&(i.HEADERS=r),o=JSON.stringify(i)),n.send(o,e),t&&u.shouldCancel(t,e)&&t.preventDefault())))})}))})}}})}();
|
Loading…
Reference in New Issue
Block a user