First pass at sidebar / rendering logic
This commit is contained in:
parent
20ac362a41
commit
74add41ccd
@ -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
12
ext/sidebar.html
Normal 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>
|
@ -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"}}}}}]})
|
||||
|
@ -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)
|
||||
|
39
src/looped_in/hackernews.cljs
Normal file
39
src/looped_in/hackernews.cljs
Normal 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)))))))
|
@ -1,4 +0,0 @@
|
||||
(ns looped-in.popup
|
||||
(:require [looped-in.logging :as log]))
|
||||
|
||||
(log/info "Hello from the popup script!")
|
73
src/looped_in/sidebar.cljs
Normal file
73
src/looped_in/sidebar.cljs
Normal 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)))
|
Loading…
Reference in New Issue
Block a user