First pass at sidebar / rendering logic

This commit is contained in:
Jeremy Dormitzer 2018-01-12 13:29:12 -05:00
parent 20ac362a41
commit 74add41ccd
No known key found for this signature in database
GPG Key ID: 04F17C0F5A32C320
7 changed files with 168 additions and 55 deletions

View File

@ -12,7 +12,8 @@
"background": {
"scripts": [
"js/browser-polyfill.min.js",
"js/generated/out/cljs_base.js",
"js/generated/out/goog/base.js",
"js/generated/out/cljs_deps.js",
"js/generated/background.js"
]
},
@ -20,7 +21,15 @@
"default_icon": "icons/icon48.png",
"default_title": "Looped In"
},
"sidebar_action": {
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png"
},
"default_title": "Looped In",
"default_panel": "sidebar.html"
},
"permissions": ["tabs", "https://hn.algolia.com/*"],
"content_security_policy": "script-src 'self'; object-src 'self'; connect-src 'self' https://hn.algolia.com",
"web_accessible_resources": ["js/*.map", "js/generated/*.map"]
"content_security_policy": "script-src 'self' 'unsafe-eval' 'sha256-CfMPE8ys/ylJ5D9qKG0a9/UejrcczMr4/EmFCbVbgcc=' 'sha256-XsBu2nEJnS7x/Izq1v7dzy3Ze5myJMHvg4zukh/R1Yk=' 'sha256-AUCho1UyOpYFSZDg8EM9SYlysRIrUpQKZ7iE9CFSYfU=' 'sha256-vLlTaRFN8A2FPadIx711FwK9Ytd6LXkAzuEYAdC0D1k='; object-src 'self'; connect-src 'self' https://hn.algolia.com",
"web_accessible_resources": ["js/*.map", "js/generated/*.map", "js/generated/out/*"]
}

12
ext/sidebar.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="js/generated/out/goog/base.js"></script>
<script src="js/generated/out/cljs_deps.js"></script>
<script src="js/generated/sidebar.js"></script>
</head>
<body>
<div id="storiesContainer"></div>
</body>
</html>

View File

@ -10,13 +10,13 @@
[cljs-ajax "0.7.3"]]
:plugins [[lein-cljsbuild "1.1.7"]]
:cljsbuild {:builds [{:source-paths ["src"]
:compiler {:optimizations :simple
:compiler {:optimizations :none
:pretty-print true
:source-map true
:output-dir "ext/js/generated/out"
:modules {:background
{:output-to "ext/js/generated/background.js"
:entries #{"looped-in.background"}}
:popup
{:output-to "ext/js/generated/popup.js"
:entries #{"looped-in.popup"}}}}}]})
:sidebar
{:output-to "ext/js/generated/sidebar.js"
:entries #{"looped-in.sidebar"}}}}}]})

View File

@ -1,46 +1,14 @@
(ns looped-in.background
(:require [clojure.core.match :refer [match]]
[cljs.core.async :refer [go go-loop chan close! >! <!]]
[cljs.core.async :refer [go <!]]
[ajax.core :refer [GET]]
[looped-in.hackernews :as hn]
[looped-in.logging :as log]
[looped-in.promises :refer [channel->promise promise->channel]]))
(enable-console-print!)
(defn fetch-submission
"Fetches submissions from Hacker News by `url`"
[url]
(let [response-chan (chan)]
(GET "https://hn.algolia.com/api/v1/search"
{:params {"query" url
"hitsPerPage" 1000
"restrictSearchableAttributes" "url"}
:handler (fn [res] (go (>! response-chan res)))
:error-handler (fn [err]
(log/error (str "Error fetching HN stories for " url ":") err)
(close! response-chan))})
response-chan))
(defn fetch-item
"Fetches items from Hacker News by `id`"
[id]
(let [response-chan (chan)]
(GET (str "https://hn.algolia.com/api/v1/items/" id)
{:handler (fn [res] (go (>! response-chan res)))
:error-handler (fn [err]
(log/error (str "Error fetching item " id ":") err)
(close! response-chan))})
response-chan))
(defn fetch-items-for-hits [hits]
(let [chans (map (fn [hit]
(fetch-item (hit "objectID")))
hits)]
(go-loop [[channel & rest] chans
acc []]
(if (nil? channel)
acc
(recur rest (conj acc (<! channel)))))))
(def object-ids (atom []))
(defn url-path
"Returns a url without its protocol"
@ -68,9 +36,7 @@
(.setBadgeBackgroundColor (.-browserAction js/browser) #js {:color "#232323"})
(.setBadgeText (.-browserAction js/browser) #js {:text text}))
;; TODO memoize handle-update, but with a 5-minute expiration
(defn handle-update [tab-id]
(defn handle-tab-update [tab-id]
(go (let [url (-> js/browser
(.-tabs)
(.query #js {:active true :currentWindow true})
@ -79,24 +45,42 @@
(first)
(.-url))
hits (-> url
(fetch-submission)
(hn/fetch-submission)
(<!)
(filter-response url)
(sort-hits))
items (<! (fetch-items-for-hits hits))
ids (map #(% "objectID") hits)
num-comments (total-num-comments hits)]
(set-badge-text! (str num-comments))
(log/debug items))))
(reset! object-ids ids)
(set-badge-text! (str num-comments)))))
(defn handle-message [msg]
(match (.-type msg)
"popupOpened" (channel->promise (go @object-ids))
x (log/error "Unknown popup message type" x)))
(defn handle-browser-action [tab]
(-> js/browser (.-sidebarAction) (.open)))
(-> js/browser
(.-tabs)
(.-onActivated)
(.addListener handle-update))
(.addListener handle-tab-update))
(-> js/browser
(.-tabs)
(.-onUpdated)
(.addListener handle-update))
(.addListener handle-tab-update))
(-> js/browser
(.-runtime)
(.-onMessage)
(.addListener handle-message))
(-> js/browser
(.-browserAction)
(.-onClicked)
(.addListener handle-browser-action))
;; Application logic:
;; 1. Event comes in (new url)

View File

@ -0,0 +1,39 @@
(ns looped-in.hackernews
(:require [cljs.core.async :refer [go go-loop >! chan close!]]
[ajax.core :refer [GET]]
[looped-in.logging :as log]))
(defn fetch-submission
"Fetches submissions from Hacker News by `url`"
[url]
(let [response-chan (chan)]
(GET "https://hn.algolia.com/api/v1/search"
{:params {"query" url
"hitsPerPage" 1000
"restrictSearchableAttributes" "url"}
:handler (fn [res] (go (>! response-chan res)))
:error-handler (fn [err]
(log/error (str "Error fetching HN stories for " url ":") err)
(close! response-chan))})
response-chan))
(defn fetch-item
"Fetches items from Hacker News by `id`"
[id]
(let [response-chan (chan)]
(GET (str "https://hn.algolia.com/api/v1/items/" id)
{:handler (fn [res] (go (>! response-chan res)))
:error-handler (fn [err]
(log/error (str "Error fetching item " id ":") err)
(close! response-chan))})
response-chan))
(defn fetch-items [ids]
(let [chans (map (fn [id]
(fetch-item id))
ids)]
(go-loop [[channel & rest] chans
acc []]
(if (nil? channel)
acc
(recur rest (conj acc (<! channel)))))))

View File

@ -1,4 +0,0 @@
(ns looped-in.popup
(:require [looped-in.logging :as log]))
(log/info "Hello from the popup script!")

View File

@ -0,0 +1,73 @@
(ns looped-in.sidebar
(:require [goog.dom :as dom]
[goog.html.sanitizer.HtmlSanitizer :as Sanitizer]
[cljs.core.async :refer [go <!]]
[looped-in.hackernews :as hn]
[looped-in.promises :refer [promise->channel]])
(:import (goog.ui Zippy)))
(defn log [& args]
(let [bg (-> js/browser (.-extension) (.getBackgroundPage))]
(apply (-> bg (.-console) (.-log)) "[Looped In]" (map clj->js args))))
(defn comment-dom [{:strs [text author children]}]
(let [$text (dom/createDom "div"
#js {:class "commentText"}
(dom/safeHtmlToNode (Sanitizer/sanitize text)))
$author (dom/createDom "div"
#js {:class "commentAuthor"}
author)]
(if (> (count children) 0)
(let [$toggle (dom/createDom "div"
#js {:class "commentToggle"}
"<Toggle children>")
$children (apply dom/createDom
"div"
#js {:class "commentChildren"}
(clj->js (map comment-dom children)))]
(Zippy. $toggle $children)
(dom/createDom "div"
#js {:class "comment"}
$text
$author
$toggle
$children))
(dom/createDom "div"
#js {:class "comment"}
$text
$author))))
(defn comments-dom [comments]
(clj->js
(apply dom/createDom
"div"
#js {:class "comments"}
(map comment-dom comments))))
(defn story-dom [story]
(let [$title (dom/createDom "div"
#js {:class "storyTitle"}
(story "title"))
$comments (comments-dom (filter #(= "comment" (% "type")) (story "children")))]
(Zippy. $title $comments)
(dom/createDom "div"
#js {:class "story"}
$title
$comments)))
(defn render-items [items]
(let [stories (filter #(= "story" (% "type")) items)
$stories (clj->js (map story-dom stories))
$storiesContainer (dom/getElement "storiesContainer")]
(log items)
(dom/append $storiesContainer $stories)))
(go (-> js/browser
(.-runtime)
(.sendMessage #js {:type "popupOpened"})
(promise->channel)
(<!)
(hn/fetch-items)
(<!)
((fn [items] (filter #(not (nil? %)) items)))
(render-items)))