diff options
| author | Scott Ostler <sostler@deathmachine.local> | 2010-03-11 08:02:51 -0500 |
|---|---|---|
| committer | Scott Ostler <sostler@deathmachine.local> | 2010-03-11 08:02:51 -0500 |
| commit | 7d969b406bc472f86617df9697ad19f6dd15008d (patch) | |
| tree | ad0fb996fde2b59f4a14037e7ffd3b80084a6011 | |
| parent | 3d83db95469600000f4fbc1337bfc6aac9efd9a4 (diff) | |
| parent | fefadd9fb388e04c2ecc4b9757138c097572e2d1 (diff) | |
Merge site.clj
| -rwxr-xr-x | src/site.clj | 62 | ||||
| -rw-r--r-- | static/tests/scrolling.html | 437 | ||||
| -rw-r--r-- | template/directory.st | 1 |
3 files changed, 445 insertions, 55 deletions
diff --git a/src/site.clj b/src/site.clj index d05686b..9a1ac79 100755 --- a/src/site.clj +++ b/src/site.clj @@ -81,7 +81,7 @@ (defn today [] (.format YYYYMMDD-format (new Date))) -(def formatter (new SimpleDateFormat "h:mm EEE M/d")) +(def formatter (new SimpleDateFormat "h:mm a EEE M/d")) (defn non-empty-string? [s] (and s (> (count s) 0))) @@ -134,6 +134,9 @@ ;; Output +(defn stringify-and-escape [m] + (zipmap (map name (keys m)) (map escape-html (vals m)))) + (defn process-message-for-json [d] (assoc d :created_on (.getTime (d :created_on)))) @@ -366,7 +369,7 @@ (with-connection *db* (update-values "users" ["user_id = ?" user-id] {attr val}))) -(defn update-avatar [session url] +(defn update-avatar [session url] (update-user-db (session :user_id) "avatar" url) [(session-assoc :avatar url) (resp-success url)]) @@ -385,23 +388,57 @@ ;; Directory +(def *directory-agent* (agent nil)) +(def *directory-listing* (ref [])) (def *per-directory-page* 25) -(def *update-directory* true) -(def *update-directory-ms* (* 60 60 1000)) +(def *run-update-directory* true) +(def *update-directory-sleep-ms* (* 60 60 1000)) (defn directory-search [offset] - (let [qry (str "SELECT u.nick, u.avatar, m.content " + (let [directory @*directory-listing* + users (subvec directory + (* offset *per-directory-page*) + (min (count directory) + (* (inc offset) *per-directory-page*))) + user-ids (apply str (interpose ", " (map #(%1 :user_id) users))) + qry (str "SELECT u.user_id, u.nick, u.avatar, m.content " + "FROM users u, messages m " + "WHERE u.user_id in (" user-ids ") " + "AND m.user_id = u.user_id " + "AND m.created_on = (select max(created_on) from messages " + " where user_id = u.user_id)")] + (when (> (count user-ids) 0) + (let [res (do-select [qry]) + keys (map :user_id res) + dict (zipmap keys res)] + (map (fn [u] + (let [u-id (u :user_id)] + (stringify-and-escape (merge u (dict u-id))))) + users))))) + +(defn update-directory! [] + (let [qry (str "SELECT u.user_id, COUNT(m) as cnt " "FROM users u, messages m " - "WHERE m.message_id = " - " (SELECT n.message_id FROM messages n " - " WHERE n.user_id = u.user_id ORDER BY n.created_on DESC LIMIT 1) " - "ORDER BY (SELECT COUNT(*) FROM messages where user_id = u.user_id) " - "LIMIT ? OFFSET ?")] - (do-select [qry *per-directory-page* (* offset *per-directory-page*)]))) + "WHERE u.user_id = m.user_id " + "GROUP BY u.user_id " + "ORDER BY COUNT(m) DESC") + res (vec (do-select [qry]))] + (dosync (ref-set *directory-listing* res)) + res)) + +(defn update-directory-agent-func [x] + (update-directory!) + (Thread/sleep *update-directory-sleep-ms*) + (when *run-update-directory* + (send *directory-agent* #'update-directory-agent-func)) + x) + +(defn start-directory-updater! [] + (send *directory-agent* update-directory-agent-func)) (defn directory [session offset] (let [st (fetch-template "directory" session) - users (to-array (map process-directory-listing (directory-search offset)))] + users (to-array (directory-search offset))] (.setAttribute st "users" users) (cond (= offset 0) (.setAttribute st "prev" false) (= offset 1) (.setAttribute st "prev" "") @@ -825,4 +862,5 @@ (start-user-flusher!) (start-session-pruner!) +(start-directory-updater!) (start-server (options :port))
\ No newline at end of file diff --git a/static/tests/scrolling.html b/static/tests/scrolling.html index 00e7b89..bd0a08f 100644 --- a/static/tests/scrolling.html +++ b/static/tests/scrolling.html @@ -1,8 +1,35 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" +"http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script> <script> +/* scrolling: + +focus is either on a specific message or at the end + +when to recalculate focus? +* user scrolls +* we force a scroll to end + +when to adjust scroll to focused message? +* page loaded +* window resized +* messages added +* messages deleted +* images loaded + +*/ + +// TODO: figure out why firefox is being a fucker with regard to changing the heights +// of things when elements are removed from above + +Array.prototype.random = function() { + return this[Math.floor((Math.random()*this.length))]; +} + +// image fetch stuff var imageQueue = [] function fetch(){ @@ -14,35 +41,24 @@ function fetch(){ } function fetched(data){ - log("images fetched") var imageUrls = [] try { var images = data['value']['items'][0]['recent-images']['recent-image'] for(var i = 0; i < images.length; i++) - imageQueue.push(images[i]['img']) + imageQueue.push(images[i]['img']); + log(images.length + " images fetched") } catch(e) { + log("couldn't parse images :(") console.log("couldn't parse object:") console.log(data) } } -function log(m){ - $("#log").html(m) -} - -function go(){ - messagePane = $("#chat")[0] - maxImages = $("#max-images")[0] - imagePoster() - scrollToEnd() - scrollWatcher() -} - function imagePoster(){ if (!imageQueue.length){ - log("queue empty") + //log("queue empty") } else if (Paused) { - log("paused") + //log("paused") } else { log(imageQueue.length + " images in queue ... posting image") var image = imageQueue.shift() @@ -53,7 +69,7 @@ function imagePoster(){ imagePosts = 0 function imagePost(image){ - imagePosts += 1 + /*imagePosts += 1 while (imagePosts > maxImages.value) { var imgs = $(".image-post:first") if (imgs.length) { @@ -62,9 +78,9 @@ function imagePost(image){ break } imagePosts -= 1 - } + }*/ var i = $("<img/>").attr("src", image) //.load(scrollIfPossible).error(scrollIfPossible) - var d = $("<div class='image-post'/>").html("username: ") + var d = $("<div class='image-post msg'/>").html("username: ") d.append(i).appendTo("#chat") } @@ -74,53 +90,388 @@ function pausego(){ $("#pausego-button").html(Paused ? "go" : "pause") } -function isScrolledToBottom(){ - var threshold = 50; +// end image fetch stuff + +function log(html, klass){ + klass = klass || "" + klass = 'log-message ' + klass + var html = "<p class='" + klass + "'>" + html + "</p>" + scrollLog.append(html) + scrollLog[0].scrollTop = scrollLog[0].scrollHeight +} + +function go(){ + messagePane = $("#chat")[0] + maxImages = $("#max-images")[0] + scrollLog = $("#scroll-log") + $("#chat").scroll(eventScroll) + $(window).resize(windowResizedHandler) + + fetch() + + //imagePoster() + + messagesLoadFromPage() + + scrollToEnd() + setLastKnownHeight() + setLastKnownScrollTop() + messageHeightsCalc() + highlightFocusedMessage() - var containerHeight = messagePane.style.pixelHeight || messagePane.offsetHeight - var currentHeight = (messagePane.scrollHeight > 0) ? messagePane.scrollHeight : 0 + + + + //scrollWatcher() +} + +// messages stuff + +// Messages = [{element: aDomNode, complete: false, height: 24, imgs: []}, {...}, ...] +var Messages = [] + +function messageRandomButton(){ + messageRandomButton.paused = !messageRandomButton.paused + + if (messageRandomButton.paused) + clearTimeout(messageRandom.timer) + else + messageRandom() - var result = (currentHeight - messagePane.scrollTop - containerHeight < threshold); + $("#message-random-button").html((messageRandomButton.paused ? "start" : "stop") + " adding + removing messages") - return result; } +messageRandomButton.paused = true -function scrollIfPossible(){ - if (lastScriptedScrolledPosition <= messagePane.scrollTop || isScrolledToBottom()) - scrollToEnd() +function messageRandom(){ + var funcs = [messageRemoveFirst, messageAddNew, messageAddNew, messageAddNew, messageAddNew] + funcs.random()(); + messageRandom.timer = setTimeout(messageRandom, 100) +} + +function messageRandomImageButton(){ + messageRandomImageButton.paused = !messageRandomImageButton.paused + + if (messageRandomImageButton.paused) + clearTimeout(messageRandomImage.timer) + else + messageRandomImage() + + $("#message-random-image-button").html((messageRandomImageButton.paused ? "start" : "stop") + " adding + removing image messages") + +} +messageRandomImageButton.paused = true + +function messageRandomImage(){ + var funcs = [messageRemoveFirst, messageAddNew, messageAddNew, messageImageAddNew, messageImageAddNew] + funcs.random()(); + messageRandomImage.timer = setTimeout(messageRandomImage, 100) +} + +function messageRemoveFirst(){ + //$(".msg:first").remove() + var msg = Messages.shift() + $(msg['element']).remove() + LastKnown.focus -= 1 + scrollToFocus(true) +} + +function messagesLoadFromPage(){ + Messages = [] + $(".msg").each(function(){ + Messages.push({"element": this, "complete": true}) + }) +} + +function messageAddNew(nodesToAppend){ + var string = " " + Math.random() + " " + Math.random() + " " + Math.random() + " " + Math.random() + " " + Math.random() + " " + Math.random() + " " + Math.random() + " " + Math.random() + var msg = $("<div class='msg'>test" + string + "</div>") + if (nodesToAppend && nodesToAppend.length){ + for(var i = 0; i < nodesToAppend.length; i++){ + nodesToAppend[i].appendTo(msg) + } + } + msg.appendTo("#chat") + var imgs = msg.find("img") + if (imgs.length) { + complete = false + var truImgs = [] + for (var i = 0; i < imgs.length; i++){ + truImgs.push(imgs[i]) + } + Messages.push({"element": msg[0], "height": msg.outerHeight(true), "complete": false, "imgs": truImgs}) + startImageCompletenessMonitor() + // start completeness monitor + } else { + Messages.push({"element": msg[0], "height": msg.outerHeight(true), "complete": true}) + } + + scrollToFocus() +} + +function messageImageAddNew(){ + if (!imageQueue.length) + return + var image = imageQueue.shift() + var i = $("<img/>").attr("src", image) + messageAddNew([i]) + +} + +function startImageCompletenessMonitor(){ + if (!imageCompletenessMonitor.timer) { + imageCompletenessMonitor() + log("starting image completeness monitor") + } +} + +function imageCompletenessMonitor(){ + var allMessagesComplete = true + for (var m = 0; m < Messages.length; m++){ + var msg = Messages[m]; + if (!msg["complete"]){ + allMessagesComplete = false; + var messageComplete = true; + for (var i = 0; i < msg["imgs"].length; i++){ + if (!msg["imgs"][i].complete) { + messageComplete = false; + break; + } + } + msg["complete"] = messageComplete; + msg["height"] = $(msg["element"]).outerHeight(true) + } + } + if (allMessagesComplete) { + clearTimeout(imageCompletenessMonitor.timer) + imageCompletenessMonitor.timer = 0 + log("stopping image completeness monitor") + } else { + scrollToFocus() + imageCompletenessMonitor.timer = setTimeout(imageCompletenessMonitor, 500) + } +} +imageCompletenessMonitor.timer = 0 + +// i just made this a function so i can log it... +// pass an argument to set it, otherwise returns value +function keepScrollAtBottom(){ + if (arguments.length) { + log("keep scroll at bottom was: " + keepScrollAtBottom.val + ", is now: " + arguments[0], "yellow") + keepScrollAtBottom.val = arguments[0] + } + return keepScrollAtBottom.val +} + +function logScrollProps(){ + var props = "{ scrollTop: " + messagePane.scrollTop + + ", scrollHeight: " + messagePane.scrollHeight + + ", height: " + messagePane.offsetHeight + " }" +// var total = "height+scrollTop: " + (messagePane.offsetHeight + messagePane.scrollTop) + log(props) +// log(total) +} + +// "scroll" event triggered if: +// user scrolls element +// we change element's scrollTop <-- not in opera +// scrollbar at bottom and stuff removed from top <-- not in opera +// scrollbar at bottom and window resized <-- not in opera +// i don't know what happens with regards to ie & i don't care (for now) + +function eventScroll(e){ + // added 'ignoreLastScroll' so we can ignore scroll events when we change scrollTop + if (eventScroll.ignoreLastScroll && $.browser != "opera") { + eventScroll.ignoreLastScroll = false + log("pane scrolled (ignoring)", "red") + return; + } + log("pane scrolled", "green") + findScrollFocus() + logScrollProps() + highlightFocusedMessage() +} +eventScroll.ignoreLastScroll = false + +function findScrollFocus(){ + if (messagePane.scrollHeight <= messagePane.offsetHeight) // no scrollbar exists yet... content not overflowing + keepScrollAtBottom(true) + else if (isScrolledToBottom()) + keepScrollAtBottom(true) + else + keepScrollAtBottom(false) + var i = findMiddleMessage() + LastKnown.focus = i +} + +function findMiddleMessage(){ + var midpoint = (messagePane.offsetHeight / 2) + messagePane.scrollTop + var total = 0 + for(var i = 0; i < Messages.length; i++){ + total += Messages[i].height + if (total > midpoint) { + log("found middle item after " + i + " iterations") + return i + } + } + return i +} + +function highlightFocusedMessage(){ + $(".msg").removeClass("selected") + if (keepScrollAtBottom()) + $(".msg:last").addClass("selected") + else + $(".msg:eq(" + LastKnown.focus + ")").addClass("selected") +} + +function isScrolledToBottom(){ + var threshold = 20; + // is messagePane.offsetHeight instead messagePane.style.pixelHeight in ie? + return (messagePane.scrollHeight - messagePane.scrollTop - messagePane.offsetHeight < threshold); } -var lastScriptedScrolledPosition = 0 function scrollToEnd(){ + eventScroll.ignoreLastScroll = true messagePane.scrollTop = messagePane.scrollHeight - lastScriptedScrolledPosition = messagePane.scrollTop + keepScrollAtBottom(true) } -function scrollWatcher(){ - scrollIfPossible() - setTimeout(scrollWatcher, 500) +function scrollToFocus(forceUpdate){ + if (keepScrollAtBottom()) { // scroll to bottom + scrollToEnd() + highlightFocusedMessage() + } else { // scroll to focused message + if ( forceUpdate || + LastKnown.height != messagePane.offsetHeight /* was resized */ ) { + var heightToMessage = 0 + for(var i = 0; i < LastKnown.focus; i++) + heightToMessage += Messages[i].height; + heightToMessage += /*Math.floor( */Messages[i].height / 2 //) + eventScroll.ignoreLastScroll = true + // _____________ + // X = scrollTop | | + // | a | + // X____________ | <- scrollHeight + // offsetHeight -> | | b | | + // of |____________| | + // visible | c | + // frame |_____________| + // + // to set scrollTop to center item b, take distance to item b from top - (offsetHeight / 2) + // + messagePane.scrollTop = heightToMessage - /*Math.floor (*/messagePane.offsetHeight / 2/*)*/ + highlightFocusedMessage() + setLastKnownHeight() + setLastKnownScrollTop() + } + } +} + +function messageHeightsCalc() { + log("recalculated heights of each message") + for(var i = 0; i < Messages.length; i++){ + Messages[i].height = $(Messages[i].element).outerHeight(true) + } +} + +// there's a timeout so that while the window is getting resized, it doesn't recalculate constantly, which would be slow +function windowResizedHandler(){ + log("window resized") + if (windowResizedHandler.timeout) clearTimeout(windowResizedHandler.timeout); + windowResizedHandler.timeout = setTimeout(windowResized, 250) +} + +function windowResized(){ + messageHeightsCalc() + scrollToFocus() + setLastKnownHeight() + setLastKnownScrollTop() +} + +var LastKnown = { + "height": 0, "scrollTop": 0, "focusElement": 0 +} +function setLastKnownHeight(){ + LastKnown.height = messagePane.offsetHeight +} +function setLastKnownScrollTop(){ + LastKnown.scrollTop = messagePane.scrollTop } </script> <style> - #chat { width: 500px; height: 90%; overflow: scroll; } + html, body { + height: 100%; /* need dis to enable percentage heights working w/ standard doctype */ + } + #chat { + width: 500px; + height: 85%; + overflow-y: scroll; + overflow-x: hidden; + } img { max-height: 300px; } + #sidebar { + height: 85%; float: right; + } + #scroll-log { width: 300px; height: 80%; overflow: scroll; } + .log-message { + margin: 0; padding: 0; + font-size: 10px; + } + .selected, .yellow { + background-color: yellow; + } + .green { + background-color: green; + } + .red { + background-color: red; + } </style> </head> <body> + <center><h3>scrolling tests</h3></center> + <div id="sidebar"> + <div id="scroll-log"><h3>log</h3></div> + <button onclick="messageRemoveFirst()">purge oldest message</button> + <button onclick="messageAddNew()">add new message</button> + <br> + <button onclick="messageRandomButton()" id="message-random-button">start adding + removing messages</button> + <br> + <button onclick="messageImageAddNew()">add image message</button> + <br> + <button onclick="messageRandomImageButton()" id="message-random-image-button">start adding + removing image messages</button> + </div> <div id="chat"> - test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br> - test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br> - test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br> - test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br> - test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br> - test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br>test<br> + <div class="msg">test 1</div><div class="msg">test 2</div><div class="msg">test 3</div><div class="msg">test 4</div><div class="msg">test 5</div><div class="msg">test 6</div><div class="msg">test 7</div><div class="msg">test 8</div><div class="msg">test 9</div><div class="msg">test 10</div> + <div class="msg">test 11</div><div class="msg">test 12</div><div class="msg">test 13</div><div class="msg">test 14</div><div class="msg">test 15</div><div class="msg">test 16</div><div class="msg">test 17</div><div class="msg">test 18</div><div class="msg">test 19</div><div class="msg">test 20</div> + <div class="msg">test 21</div><div class="msg">test 22</div><div class="msg">test 23</div><div class="msg">test 24</div><div class="msg">test 25</div><div class="msg">test 26</div><div class="msg">test 27</div><div class="msg">test 28</div><div class="msg">test 29</div><div class="msg">test 30</div> + <div class="msg">test 31</div><div class="msg">test 32</div><div class="msg">test 33</div><div class="msg">test 34</div><div class="msg">test 35</div><div class="msg">test 36</div><div class="msg">test 37</div><div class="msg">test 38</div><div class="msg">test 39</div><div class="msg">test 40</div> + <div class="msg">test 41</div><div class="msg">test 42</div><div class="msg">test 43</div><div class="msg">test 44</div><div class="msg">test 45</div><div class="msg">test 46</div><div class="msg">test 47</div><div class="msg">test 48</div><div class="msg">test 49</div><div class="msg">test 50</div> </div> <p id="log"></p> - <button onclick="fetch()">add images</button> - <button onclick="pausego()" id="pausego-button">pause</button> + + + <br> + + + <!-- <button onclick="fetch()">add images</button> + <button onclick="pausego()" id="pausego-button">pause</button> <br /> - max image posts: <input name="max-images" id="max-images" value="50" /> + max image posts: <input name="max-images" id="max-images" value="50" /> --> +<pre> +goals: +initial state: +* keep scrollbar at bottom while new messages are added to bottom +* keep scrollbar at bottom while old messages purged from top +* keep scrollbar at bottom while window resized +if user scrolls up: +* keep central message location consistent while new messages added to bottom +* keep central message location consistent while new messages purged from top +* keep central message location consistent while window resized +* scroll back to bottom after a reasonable period of no activity (1 minute?) +</pre> </body> <script> go() diff --git a/template/directory.st b/template/directory.st index c57c333..7f78265 100644 --- a/template/directory.st +++ b/template/directory.st @@ -43,6 +43,7 @@ <img height="50" width="50" src="$u.avatar$"></img> $endif$ </a> + <div>Count: $u.cnt$</div> <div> <span>Last post:</span> <span class="linkify">$u.content$</span> |
