add edu-feed

This commit is contained in:
@s.roertgen 2024-08-20 21:19:55 +02:00
parent 570eed0f41
commit 960200d410
11 changed files with 3755 additions and 230 deletions

2723
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@
"dependencies": {
"@noble/secp256k1": "^2.1.0",
"bech32": "^2.0.0",
"js-confetti": "^0.12.0",
"nostr-tools": "^2.7.2",
"react": "17.0.2",
"react-dom": "17.0.2"

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

View file

@ -981,6 +981,70 @@ html {
--tw-border-opacity: 0.2;
}
.collapse:not(td):not(tr):not(colgroup) {
visibility: visible;
}
.collapse {
position: relative;
display: grid;
overflow: hidden;
grid-template-rows: auto 0fr;
transition: grid-template-rows 0.2s;
width: 100%;
border-radius: var(--rounded-box, 1rem);
}
.collapse-title,
.collapse > input[type="checkbox"],
.collapse > input[type="radio"],
.collapse-content {
grid-column-start: 1;
grid-row-start: 1;
}
.collapse > input[type="checkbox"],
.collapse > input[type="radio"] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
opacity: 0;
}
.collapse-content {
visibility: hidden;
grid-column-start: 1;
grid-row-start: 2;
min-height: 0px;
transition: visibility 0.2s;
transition: padding 0.2s ease-out,
background-color 0.2s ease-out;
padding-left: 1rem;
padding-right: 1rem;
cursor: unset;
}
.collapse[open],
.collapse-open,
.collapse:focus:not(.collapse-close) {
grid-template-rows: auto 1fr;
}
.collapse:not(.collapse-close):has(> input[type="checkbox"]:checked),
.collapse:not(.collapse-close):has(> input[type="radio"]:checked) {
grid-template-rows: auto 1fr;
}
.collapse[open] > .collapse-content,
.collapse-open > .collapse-content,
.collapse:focus:not(.collapse-close) > .collapse-content,
.collapse:not(.collapse-close) > input[type="checkbox"]:checked ~ .collapse-content,
.collapse:not(.collapse-close) > input[type="radio"]:checked ~ .collapse-content {
visibility: visible;
min-height: -moz-fit-content;
min-height: fit-content;
}
.dropdown {
position: relative;
display: inline-block;
@ -1117,18 +1181,6 @@ html {
}
}
.btn-outline.btn-primary:hover {
--tw-text-opacity: 1;
color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));
}
@supports (color: color-mix(in oklab, black, black)) {
.btn-outline.btn-primary:hover {
background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black);
}
}
.btn-outline.btn-warning:hover {
--tw-text-opacity: 1;
color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity)));
@ -1372,22 +1424,6 @@ html {
opacity: 1;
}
.modal-action {
display: flex;
margin-top: 1.5rem;
justify-content: flex-end;
}
.modal-toggle {
position: fixed;
height: 0px;
width: 0px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
opacity: 0;
}
:root:has(:is(.modal-open, .modal:target, .modal-toggle:checked + .modal, .modal[open])) {
overflow: hidden;
scrollbar-gutter: stable;
@ -1473,6 +1509,20 @@ html {
border-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-border-opacity)));
}
.badge-primary {
--tw-border-opacity: 1;
border-color: var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity)));
--tw-bg-opacity: 1;
background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity)));
--tw-text-opacity: 1;
color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));
}
.badge-outline.badge-primary {
--tw-text-opacity: 1;
color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)));
}
.btm-nav > *.disabled,
.btm-nav > *[disabled] {
pointer-events: none;
@ -1506,10 +1556,6 @@ html {
border-color: var(--btn-color, var(--fallback-b2));
}
.btn-primary {
--btn-color: var(--fallback-p);
}
.btn-warning {
--btn-color: var(--fallback-wa);
}
@ -1520,11 +1566,6 @@ html {
}
@supports (color: color-mix(in oklab, black, black)) {
.btn-outline.btn-primary.btn-active {
background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black);
}
.btn-outline.btn-warning.btn-active {
background-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black);
border-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black);
@ -1542,17 +1583,7 @@ html {
outline-offset: 2px;
}
.btn-primary {
--tw-text-opacity: 1;
color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));
outline-color: var(--fallback-p,oklch(var(--p)/1));
}
@supports (color: oklch(0% 0 0)) {
.btn-primary {
--btn-color: var(--p);
}
.btn-warning {
--btn-color: var(--wa);
}
@ -1602,16 +1633,6 @@ html {
background-color: var(--fallback-bc,oklch(var(--bc)/0.2));
}
.btn-outline.btn-primary {
--tw-text-opacity: 1;
color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)));
}
.btn-outline.btn-primary.btn-active {
--tw-text-opacity: 1;
color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));
}
.btn-outline.btn-warning {
--tw-text-opacity: 1;
color: var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity)));
@ -1800,6 +1821,130 @@ html {
}
}
details.collapse {
width: 100%;
}
details.collapse summary {
position: relative;
display: block;
outline: 2px solid transparent;
outline-offset: 2px;
}
details.collapse summary::-webkit-details-marker {
display: none;
}
.collapse:focus-visible {
outline-style: solid;
outline-width: 2px;
outline-offset: 2px;
outline-color: var(--fallback-bc,oklch(var(--bc)/1));
}
.collapse:has(.collapse-title:focus-visible),
.collapse:has(> input[type="checkbox"]:focus-visible),
.collapse:has(> input[type="radio"]:focus-visible) {
outline-style: solid;
outline-width: 2px;
outline-offset: 2px;
outline-color: var(--fallback-bc,oklch(var(--bc)/1));
}
.collapse-arrow > .collapse-title:after {
position: absolute;
display: block;
height: 0.5rem;
width: 0.5rem;
--tw-translate-y: -100%;
--tw-rotate: 45deg;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
transition-duration: 150ms;
transition-duration: 0.2s;
top: 1.9rem;
inset-inline-end: 1.4rem;
content: "";
transform-origin: 75% 75%;
box-shadow: 2px 2px;
pointer-events: none;
}
.collapse-plus > .collapse-title:after {
position: absolute;
display: block;
height: 0.5rem;
width: 0.5rem;
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
transition-duration: 300ms;
top: 0.9rem;
inset-inline-end: 1.4rem;
content: "+";
pointer-events: none;
}
.collapse:not(.collapse-open):not(.collapse-close) > input[type="checkbox"],
.collapse:not(.collapse-open):not(.collapse-close) > input[type="radio"]:not(:checked),
.collapse:not(.collapse-open):not(.collapse-close) > .collapse-title {
cursor: pointer;
}
.collapse:focus:not(.collapse-open):not(.collapse-close):not(.collapse[open]) > .collapse-title {
cursor: unset;
}
.collapse-title {
position: relative;
}
:where(.collapse > input[type="checkbox"]),
:where(.collapse > input[type="radio"]) {
z-index: 1;
}
.collapse-title,
:where(.collapse > input[type="checkbox"]),
:where(.collapse > input[type="radio"]) {
width: 100%;
padding: 1rem;
padding-inline-end: 3rem;
min-height: 3.75rem;
transition: background-color 0.2s ease-out;
}
.collapse[open] > :where(.collapse-content),
.collapse-open > :where(.collapse-content),
.collapse:focus:not(.collapse-close) > :where(.collapse-content),
.collapse:not(.collapse-close) > :where(input[type="checkbox"]:checked ~ .collapse-content),
.collapse:not(.collapse-close) > :where(input[type="radio"]:checked ~ .collapse-content) {
padding-bottom: 1rem;
transition: padding 0.2s ease-out,
background-color 0.2s ease-out;
}
.collapse[open].collapse-arrow > .collapse-title:after,
.collapse-open.collapse-arrow > .collapse-title:after,
.collapse-arrow:focus:not(.collapse-close) > .collapse-title:after,
.collapse-arrow:not(.collapse-close) > input[type="checkbox"]:checked ~ .collapse-title:after,
.collapse-arrow:not(.collapse-close) > input[type="radio"]:checked ~ .collapse-title:after {
--tw-translate-y: -50%;
--tw-rotate: 225deg;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.collapse[open].collapse-plus > .collapse-title:after,
.collapse-open.collapse-plus > .collapse-title:after,
.collapse-plus:focus:not(.collapse-close) > .collapse-title:after,
.collapse-plus:not(.collapse-close) > input[type="checkbox"]:checked ~ .collapse-title:after,
.collapse-plus:not(.collapse-close) > input[type="radio"]:checked ~ .collapse-title:after {
content: "";
}
.dropdown.dropdown-open .dropdown-content,
.dropdown:focus .dropdown-content,
.dropdown:focus-within .dropdown-content {
@ -2051,12 +2196,6 @@ html {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.modal-action > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
}
@keyframes modal-pop {
0% {
opacity: 0;
@ -2162,10 +2301,6 @@ html {
padding-right: 0.438rem;
}
.btn-block {
width: 100%;
}
.btn-circle:where(.btn-xs) {
height: 1.5rem;
width: 1.5rem;
@ -2384,6 +2519,14 @@ html {
border-bottom-left-radius: 0px;
}
.visible {
visibility: visible;
}
.collapse {
visibility: collapse;
}
.relative {
position: relative;
}
@ -2392,6 +2535,10 @@ html {
z-index: 1;
}
.m-1 {
margin: 0.25rem;
}
.m-2 {
margin: 0.5rem;
}
@ -2416,14 +2563,22 @@ html {
display: flex;
}
.hidden {
display: none;
.h-48 {
height: 12rem;
}
.h-5 {
height: 1.25rem;
}
.h-64 {
height: 16rem;
}
.min-h-\[620px\] {
min-height: 620px;
}
.w-10 {
width: 2.5rem;
}
@ -2436,6 +2591,10 @@ html {
width: 13rem;
}
.w-64 {
width: 16rem;
}
.w-96 {
width: 24rem;
}
@ -2456,6 +2615,22 @@ html {
flex: none;
}
@keyframes flyIn {
0% {
transform: translateX(-100%);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
.animate-flyIn {
animation: flyIn 0.5s ease-out forwards;
}
.cursor-pointer {
cursor: pointer;
}
@ -2484,14 +2659,20 @@ html {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.gap-2 {
gap: 0.5rem;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.break-all {
word-break: break-all;
}
.rounded {
border-radius: 0.25rem;
}
@ -2526,11 +2707,26 @@ html {
background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
}
.bg-base-200 {
--tw-bg-opacity: 1;
background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity)));
}
.bg-green-500 {
--tw-bg-opacity: 1;
background-color: rgb(34 197 94 / var(--tw-bg-opacity));
}
.object-contain {
-o-object-fit: contain;
object-fit: contain;
}
.object-cover {
-o-object-fit: cover;
object-fit: cover;
}
.p-2 {
padding: 0.5rem;
}
@ -2554,6 +2750,10 @@ html {
font-weight: 700;
}
.font-medium {
font-weight: 500;
}
.text-black {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));
@ -2584,3 +2784,7 @@ html {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));
}
.hover\:underline:hover {
text-decoration-line: underline;
}

View file

@ -6,6 +6,7 @@
:show-add-event false
:events #{}
:pk nil
:sk nil
:list-kinds [30001 30004]
:default-relays [{:name "strfry-1"
:uri "http://localhost:7777"
@ -19,10 +20,16 @@
:uri "http://localhost:4445"
:id (random-uuid)
:status "disconnected"}
{:name "damus"
:uri "wss://relay.damus.io"
; {:name "damus"
; :uri "wss://relay.damus.io"
; :status "disconnected"}
{:name "SC24"
:uri "wss://relay.sc24.steffen-roertgen.de"
:status "disconnected"}
]
:selected-events #{}
:selected-lists #{}
:selected-list-ids #{}
:show-lists-modal false
:show-create-list-modal false
:show-event-data-modal false
:sockets []})

View file

@ -8,7 +8,12 @@
[promesa.core :as p]
[wscljs.client :as ws]
[wscljs.format :as fmt]))
[wscljs.format :as fmt]
[clojure.string :as str]
[clojure.set :as set]
["js-confetti" :as jsConfetti]
))
(def list-kinds [30001 30004])
@ -38,11 +43,8 @@
;; TODO if EOSE retrieved end connection identified by uri
(fn-traced [{:keys [db]} [_ [uri raw-event]]]
(let [event (nth raw-event 2 raw-event)]
(println uri raw-event event)
(when (and
(= (first raw-event) "EVENT")
;;(not (some #(= (:id event) (:id %)) (get db :events {})))
)
(= (first raw-event) "EVENT"))
{:db (update db :events conj event)}))))
(defn handlers
@ -50,7 +52,6 @@
{:on-message (fn [e] (re-frame/dispatch [::save-event [ws-uri (-> (.-data e)
js/JSON.parse
(js->clj :keywordize-keys true))]]))
; :on-open #(prn "Opening a new connection")
:on-open #(re-frame/dispatch [::load-events ws-uri])
:on-close #(prn "Closing a connection")
:on-error (fn [e] (.log js/console "Error with uri: " ws-uri (clj->js e))
@ -78,8 +79,8 @@
(let [sockets (re-frame/subscribe [::subs/sockets])
target-ws (first (filter #(= ws-uri (:uri %)) @sockets))]
(ws/send (:socket target-ws) ["REQ" "424242" {:kinds [30142]
:limit 10}] fmt/json)
(ws/send (:socket target-ws) ["REQ" "424242" {:kinds [30004 30142]
:limit 100}] fmt/json)
; (ws/close (:socket (first @sockets))) ;; should be handled otherwise (?)
)))
@ -119,9 +120,7 @@
(fn [ws-uri]
(let [sockets (re-frame/subscribe [::subs/sockets])
target-ws (first (filter #(= ws-uri (:uri %)) @sockets))]
(re-frame/dispatch [::create-websocket target-ws])
; (ws/create (:uri target-ws) (handlers (:uri target-ws)))
)))
(re-frame/dispatch [::create-websocket target-ws]))))
;; TODO use id to close socket
;; add connection status to socket
@ -163,7 +162,6 @@
(fn [[sockets signedEvent]]
(let [connected-sockets (filter #(= "connected" (:status %)) sockets)]
(doseq [socket connected-sockets]
(.log js/console "sending to relay" signedEvent)
(ws/send (:socket socket) ["EVENT" signedEvent] fmt/json)))))
(re-frame/reg-event-db
@ -195,19 +193,21 @@
(fn-traced [cofx [_ resource]]
(let [event {:kind 30142
:created_at (:now cofx)
:content "hello world"
:content ""
:tags [["d" (:id resource)]
["author" "" (:author resource)]]}]
{::publish-resource-fx event})))
{::sign-and-publish-event event})))
;; TODO maybe we need some validation before publishing
;; TODO rename function to sth like send-to-relays
(re-frame/reg-fx
::publish-resource-fx
::sign-and-publish-event
(fn [unsignedEvent]
(p/let [_ (js/console.log (clj->js unsignedEvent))
signedEvent (.nostr.signEvent js/window (clj->js unsignedEvent))]
(re-frame/dispatch [::send-to-relays signedEvent]))))
(if (nostr/valid-unsigned-nostr-event? unsignedEvent)
(p/let [_ (js/console.log (clj->js unsignedEvent))
signedEvent (.nostr.signEvent js/window (clj->js unsignedEvent))
_ (js/console.log "Signed event: " (clj->js signedEvent))]
(re-frame/dispatch [::send-to-relays signedEvent]))
(.error js/console "Event is not a valid nostr event: " (clj->js unsignedEvent)))))
(re-frame/reg-event-fx
::login-with-extension
@ -220,15 +220,16 @@
(p/let [pk (.nostr.getPublicKey js/window)]
(re-frame/dispatch [::save-pk pk]))))
(re-frame/reg-event-db
(re-frame/reg-event-fx
::save-pk
(fn-traced [db [_ pk]]
(assoc db :pk pk)))
(fn-traced [{:keys [db]} [_ pk]]
{:db (assoc db :pk pk)
:dispatch [::get-lists-for-npub (nostr/get-npub-from-pk pk)]}))
(re-frame/reg-event-db
::logout
(fn-traced [db _]
(assoc db :pk nil)))
(assoc db :pk nil :sk nil)))
(re-frame/reg-cofx
:now
@ -239,9 +240,11 @@
[json-string created_at]
(let [parsed-json (js->clj (js/JSON.parse json-string) :keywordize-keys true)
tags (into [["d" (:id parsed-json)]
["r" (:id parsed-json)]
["id" (:id parsed-json)]
["name" (:name parsed-json)]
["image" (:image parsed-json)]]
["description" (:description parsed-json "")]
["image" (:image parsed-json "")]]
cat [(map (fn [e] ["about" (:id e) (-> e :prefLabel :de)]) (:about parsed-json))
(map (fn [e] ["inLanguage" e]) (:inLanguage parsed-json))])
event {:kind 30142
@ -255,43 +258,66 @@
[(re-frame/inject-cofx :now)]
(fn-traced [cofx [_ json-string]]
(let [event (convert-amb-to-nostr-event json-string (:now cofx))]
{::publish-resource-fx event})))
(re-frame/reg-event-db
::toggle-selected-events
(fn [db [_ event]]
(js/console.log "toggling selected event with id" (:id event))
(if (some #(= event (:id %)) (:selected-events db))
(assoc db :selected-events (filter #(not= event (:id %)) (:selected-events db)))
(update db :selected-events conj event))))
{::sign-and-publish-event event})))
(re-frame/reg-event-fx
::add-resources-to-list
::toggle-selected-events
(fn [{:keys [db]} [_ event]]
(let [selected-event-ids (set (map (fn [e] (:id e)) (:selected-events db)))]
(if (and (seq (:selected-events db))
(contains? selected-event-ids (:id event)))
{:db (assoc db :selected-events (filter #(not= (:id event) (:id %)) (:selected-events db)))}
{:db (update db :selected-events conj event)}))))
(re-frame/reg-event-db
::toggle-selected-list-ids
(fn [db [_ id]]
(println "Toggle list ids: " id)
(let [in-selected-list-ids (contains? (:selected-list-ids db) id)]
(if in-selected-list-ids
(update db :selected-list-ids disj id)
(update db :selected-list-ids conj id)))))
(re-frame/reg-event-fx
::add-metadata-event-to-list
[(re-frame/inject-cofx :now)]
(fn [cofx [_ [list resources-to-add]]]
(let [tags (into [["d" (:d list)
"name" (:name list)]]
(map (fn [e] (cond
(= 1 (:kind e)) ["e" (:id e)]
(= 30142 (:kind e)) ["a" (str "30142:" (:id e) ":" (second (first (filter #(= "d" (first %)) (:tags e)))))]))
resources-to-add))
_ (.log js/console (clj->js tags))
(let [existing-tags (:tags list)
existing-tags-set (set existing-tags)
tags-to-add (filter #(not (contains? existing-tags-set %))
(map (fn [e] (cond
(= 1 (:kind e)) ["e" (:id e)]
(= 30142 (:kind e)) (nostr/build-kind-30142-tag e)))
resources-to-add))
new-tags (vec (concat existing-tags tags-to-add))
event {:kind 30004
:created_at (:now cofx)
:content ""
:tags tags}]
{::publish-resource-fx event})))
:tags new-tags}]
{::sign-and-publish-event event})))
(re-frame/reg-event-fx
::add-metadata-events-to-lists
(fn [cofx [_ [events lists]]]
(let [dispatch-events (mapv (fn [l] [::add-metadata-event-to-list [l events]]) lists)
_ (.log js/console (clj->js dispatch-events))]
{:fx [[:dispatch-n dispatch-events]]})))
(defn sanitize-subscription-id [s]
(str/join "" (take 64 s)))
(defn make-sub-id [prefix id]
(-> (str prefix id)
(sanitize-subscription-id)))
(re-frame/reg-event-fx
::get-lists-for-npub
(fn [cofx [_ [sockets npub]]]
(println "query for lists")
(fn [{:keys [db]} [_ npub]]
(let [query-for-lists ["REQ"
"RAND24" ;; TODO maybe make this more explicit later
(make-sub-id "lists-" npub) ;; TODO maybe make this more explicit later
{:authors [(nostr/get-pk-from-npub npub)]
:kinds list-kinds}]]
(.log js/console (clj->js query-for-lists))
:kinds list-kinds}]
sockets (:sockets db)]
{::request-from-relay [sockets query-for-lists]
:dispatch [::get-deleted-lists-for-npub [sockets npub]]})))
@ -299,16 +325,16 @@
::get-deleted-lists-for-npub
(fn [cofx [_ [sockets npub]]]
(let [query-for-deleted-lists ["REQ"
"RAND24" ;; TODO maybe make this more explicit later
(make-sub-id "deleted-lists-" npub) ;; TODO maybe make this more explicit later
{:authors [(nostr/get-pk-from-npub npub)]
:kinds [5]}]]
(.log js/console "Query for deleted lists" (clj->js query-for-deleted-lists))
{::request-from-relay [sockets query-for-deleted-lists]})))
(comment)
(re-frame/reg-fx
::request-from-relay
(fn [[sockets query]]
(println "requesting from relay this query: " query)
(doall
(for [s (filter (fn [s] (= "connected" (:status s))) sockets)]
(ws/send (:socket s) query fmt/json)))))
@ -322,6 +348,26 @@
{::request-from-relay [sockets query]})))
(defn cleanup-list-name [s]
(-> s
(str/replace #"\s" "-")
(str/replace #"[^a-zA-Z0-9]" "-")))
(comment
(cleanup-list-name "this is gönna be an awesüm+ l]st"))
(re-frame/reg-event-fx
::create-new-list
[(re-frame/inject-cofx :now)]
(fn [cofx [_ name]]
(let [tags [["d" (cleanup-list-name name)]
["name" name]]
create-list-event {:kind 30004
:created_at (:now cofx)
:content ""
:tags tags}]
{::sign-and-publish-event create-list-event})))
(re-frame/reg-event-fx
::delete-list
[(re-frame/inject-cofx :now)]
@ -336,5 +382,66 @@
["a" (str (:kind l) ":" (:pubkey l) ":" (second (first (filter
#(= "d" (first %))
(:tags l)))))])]}]
(println deletion-event)
{::publish-resource-fx deletion-event})))
{::sign-and-publish-event deletion-event})))
(re-frame/reg-event-db
::toggle-show-lists-modal
(fn [db _]
(assoc db :show-lists-modal (not (:show-lists-modal db)))))
(re-frame/reg-event-db
::toggle-show-create-list-modal
(fn [db _]
(assoc db :show-create-list-modal (not (:show-create-list-modal db)))))
(re-frame/reg-event-db
::toggle-show-event-data-modal
(fn [db [_ event]]
(assoc db
:show-event-data-modal (not (:show-event-data-modal db))
:selected-event event)))
(re-frame/reg-event-fx
::delete-event-from-list
[(re-frame/inject-cofx :now)]
(fn [cofx [_ [event list]]]
(let [filtered-tags (filterv (fn [t] (not= (:id event) (nostr/extract-id-from-tag (second t)))) (:tags list))
_ (println (:tags list))
_ (.log js/console "Filtered Tags: " (clj->js filtered-tags))
event {:kind 30004
:created_at (:now cofx)
:content ""
:tags filtered-tags}]
{::sign-and-publish-event event})))
(re-frame/reg-event-db
::create-sk
(fn [db [_]]
(let [sk (nostr/generate-sk)
pk (nostr/get-pk-from-sk sk)]
(assoc db :sk sk :pk pk))))
(re-frame/reg-fx
::get-amb-json-from-uri
(fn [uri]
(p/let [raw-html (js/fetch uri {:headers {"Access-Control-Allow-Origin" "*"}})]
(println raw-html))))
(comment
(p/->> (js/fetch "https://oersi.org/resources/aHR0cHM6Ly9lZ292LWNhbXB1cy5vcmcvY291cnNlcy9hcmJlaXRlbnVuZGZ1ZWhyZW5fdXBfMjAyMi0x")
(println)))
(re-frame/reg-event-fx
::publish-amb-uri-as-nostr-event
(fn [db [_ uri]]
{::get-amb-json-from-uri uri}))
(re-frame/reg-event-fx
::add-confetti
(fn [_ _]
;; Initialize jsConfetti instance if needed
(let [confetti-instance (new jsConfetti)]
;; Trigger the confetti
(.addConfetti confetti-instance ))
;; No further effects needed
{}))

View file

@ -1,8 +1,11 @@
(ns ied.nostr
(:require
[promesa.core :as p]
[clojure.string :as str]
["@noble/secp256k1" :as secp]
["nostr-tools/nip19" :as nip19]))
["nostr-tools/nip19" :as nip19]
["nostr-tools/pure" :as nostr]
[cljs.core :as c]))
(defn event-to-serialized-json [m]
(let [event [0
@ -36,8 +39,32 @@
(defn signEvent [hashedEvent sk]
(.sign secp hashedEvent sk))
(defn generate-sk []
;; Generate a secure (private) key as byte array
(.utils.randomPrivateKey secp))
(defn sk-as-hex [sk]
(byte-array-to-hex sk))
(defn sk-as-nsec [sk]
;; let nsec = nip19.nsecEncode(sk)
;; sk should be byte-array
(.nsecEncode nip19 sk))
(defn nsec-as-sk [nsec]
;; byte array is returned
(.-data (.decode nip19 nsec)))
(defn get-pk-from-sk [sk]
(.getPublicKey nostr sk))
(comment
(.getPublicKey secp (.utils.randomPrivateKey secp)))
(let [nsec "nsec16f87vq2xvvus3qxxtmdhgvq6pyfmn7nr9ck9yw8sc8ar7xradmss66z5fz"]
(sk-as-hex (nsec-as-sk nsec)))
(.getPublicKey secp (.utils.randomPrivateKey secp))
(byte-array-to-hex (.utils.randomPrivateKey secp))
(get-pk-from-sk (generate-sk)))
(comment
(p/let [event {:content "hello world"
@ -61,6 +88,105 @@
(defn get-pk-from-npub [npub]
(.-data (.decode nip19 npub)))
;; TODO make multimethod?
(defn get-list-name [list]
(or (second (first (filter #(= "title" (first %)) (:tags list))))
(second (first (filter #(= "alt" (first %)) (:tags list))))
(second (first (filter #(= "d" (first %)) (:tags list))))
(str "No name found for List-ID: " (:id list))))
(defn get-d-id-from-event [event]
(second (first (filter #(= "d" (first %)) (:tags event)))))
(defn get-name-from-metadata-event [event]
(or (second (first (filter #(= "name" (first %)) (:tags event))))
(second (first (filter #(= "id" (first %)) (:tags event))))
(str "No name found for Metadata-Event: " (:id event))))
(defn get-image-from-metadata-event [event]
(or (let [img-url (second (first (filter #(= "image" (first %)) (:tags event))))]
(if (= "" img-url ) false img-url))
"/assets/edu-feed-logo.webp"))
(defn get-description-from-metadata-event [event]
(or (second (first (filter #(= "description" (first %)) (:tags event))))
(str "No description found for Metadata-Event: " (:id event))))
(defn get-about-tags-from-metadata-event [event]
(filter #(= "about" (first %)) (:tags event)))
(defn get-about-names-from-metadata-event [event]
(->> (get-about-tags-from-metadata-event event)
(map #(nth % 2 nil))))
(comment
(get-npub-from-pk "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d")
(get-pk-from-npub "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6"))
(defn valid-timestamp? [t]
(and (integer? t)
(pos? t)
(< t (* 1e10))))
(defn valid-kind? [k]
(integer? k))
(defn valid-tags? [tags]
(and (vector? tags)
(every? (fn [tag]
(and
(vector? tag)
(every? some? tag)))
tags)))
(defn valid-unsigned-nostr-event? [event]
(let [{:keys [created_at kind tags content]} event]
(and
(valid-timestamp? created_at)
(valid-kind? kind)
(valid-tags? tags)
(string? content))))
(comment
(valid-tags? [["hello" "there"] ["fa" ""]]))
(defn get-tag-value [tags tag-prefix]
(some #(when (= tag-prefix (first %)) (second %)) tags))
(defn sort-lists [events]
(sort-by (fn [event]
(let [tags (:tags event)
name-value (get-tag-value tags "name")
d-value (get-tag-value tags "d")]
(or name-value d-value)))
events))
;; TODO rename to ..."from-d-tag"
(defn extract-id-from-tag
[s]
(let [parts (str/split s #":")]
(if (>= (count parts) 2)
(second parts)
s)))
(comment
(extract-id-from-tag "30142:29b2dc8b83e3f8c79a9ee2535b4adb6105b90af893612e72f675a5d16f8544b5:https://wtcs.pressbooks.pub/digitalliteracy/"))
(defn list-contains-metadata-event? [list event]
(let [tags (:tags list)
metadata-event-ids-in-list (set (map extract-id-from-tag (filter (fn [t] (= "a" (first t))) tags)))
event-id (:id event)]
(.log js/console "metadata event ids in list" metadata-event-ids-in-list)
(and
(seq metadata-event-ids-in-list)
(contains? metadata-event-ids-in-list event-id))))
(defn build-kind-30142-tag [event]
["a" (str "30142:" (:id event) ":" (second (first (filter #(= "d" (first %)) (:tags event)))))])
(defn get-event-ids-from-list [list]
(->> (:tags list)
(filter #(= (or "a" "e") (first %))) ;; just a and e tags
(map second) ;; just the id
(map extract-id-from-tag)
(set)))

View file

@ -13,13 +13,17 @@
["/"
{"" :home
"add-resource" :add-resource
"keys" :keys
"feed" :event-feed
"about" :about
"settings" :settings
["" [#"npub1[ac-hj-np-z02-9]{58}" :npub]] {"" :npub-view
"/" :npub-view}}]))
"/" :npub-view}}]))
(comment
(parse "/npub1r30l8j4vmppvq8w23umcyvd3vct4zmfpfkn4c7h2h057rmlfcrmq9xt9ma/"))
(parse "/npub1r30l8j4vmppvq8w23umcyvd3vct4zmfpfkn4c7h2h057rmlfcrmq9xt9ma/")
(apply url-for [:npub-view :npub "npub1r30l8j4vmppvq8w23umcyvd3vct4zmfpfkn4c7h2h057rmlfcrmq9xt9ma"])
(apply url-for [:home]))
(defn parse
[url]
@ -39,7 +43,7 @@
(defn navigate!
[handler]
(pushy/set-token! history (url-for handler)))
(pushy/set-token! history (apply url-for handler)))
(defn start!
[]

View file

@ -2,7 +2,8 @@
(:require
[re-frame.core :as re-frame]
[clojure.string :as str]
[clojure.set :as set]))
[clojure.set :as set]
[ied.nostr :as nostr]))
(re-frame/reg-sub
::name
@ -19,17 +20,17 @@
(fn [db _]
(:sockets db)))
(re-frame/reg-sub
::connected-sockets
:<- [::sockets]
(fn [[sockets]]
(filter #(= (:status %) "connected") sockets)))
(re-frame/reg-sub
::events
(fn [db _]
(:events db)))
(re-frame/reg-sub
::metadata-events
(fn [db _]
(sort-by :created_at #(> %1 %2) (filter (fn [e] (= 30142 (:kind e))) (:events db)))))
(re-frame/reg-sub
::show-add-event
(fn [db _]
@ -40,6 +41,16 @@
(fn [db _]
(:pk db)))
(re-frame/reg-sub
::npub
(fn [db _]
(nostr/get-npub-from-pk (:pk db))))
(re-frame/reg-sub
::nsec
(fn [db _]
(nostr/sk-as-nsec (:sk db))))
(re-frame/reg-sub
::default-relays
(fn [db _]
@ -50,6 +61,18 @@
(fn [db _]
(:selected-events db)))
(re-frame/reg-sub
::selected-list-ids
(fn [db]
(:selected-list-ids db)))
(re-frame/reg-sub
::selected-lists
:<- [::lists-of-user]
:<- [::selected-list-ids]
(fn [[lists-of-user selected-list-ids]]
(filter #(contains? selected-list-ids (:id %)) lists-of-user)))
(re-frame/reg-sub
::route-params
(fn [db _]
@ -64,15 +87,24 @@
[tags]
(second (first (filter (fn [t] (= "d" (first t))) tags))))
(comment
(get-d-id-from-tags [["d" "https://wtcs.pressbopub/digitalliteracy/"]]))
(defn d-id-not-in-deleted-list-ids
[d-id deleted-list-ids]
(println d-id
deleted-list-ids)
(not (contains? deleted-list-ids d-id)))
(defn most-recent-by-d-tag
[events]
(->> events
(group-by (fn [event]
(some (fn [[tag-type tag-value]]
(when (= tag-type "d")
tag-value))
(:tags event))))
(map (fn [[d-tag events]]
(apply max-key :created_at events)))
(into [])))
(re-frame/reg-sub
::lists
:<- [::list-kinds]
@ -80,9 +112,38 @@
:<- [::deleted-list-ids]
(fn [[list-kinds events deleted-lists]]
(let [all-lists (filter #(and (some #{(:kind %)} list-kinds)
(d-id-not-in-deleted-list-ids (get-d-id-from-tags (:tags %)) deleted-lists))
events)]
all-lists)))
#_(d-id-not-in-deleted-list-ids (get-d-id-from-tags (:tags %)) deleted-lists))
events)
most-recent-lists (most-recent-by-d-tag all-lists)]
(.log js/console "all lists: " (clj->js all-lists))
(.log js/console "most recent lists: " (clj->js most-recent-lists))
most-recent-lists)))
(re-frame/reg-sub
::feed-events
:<- [::metadata-events]
:<- [::lists]
(fn [[md-events lists]]
(sort-by :created_at #(> %1 %2) (concat md-events lists))))
(re-frame/reg-sub
::lists-of-user
:<- [::pk]
:<- [::lists]
(fn [[pk lists]]
(set (filter #(= pk (:pubkey %)) lists))))
(re-frame/reg-sub
::lists-for-npub
:<- [::route-params]
:<- [::lists]
(fn [[route-params lists]]
(let [npub (:npub route-params)]
(set (filter #(= (nostr/get-pk-from-npub npub) (:pubkey %)) lists)))))
(comment
(seq [1])
(seq #{1}))
(defn extract-d-id-from-tags
[s]
@ -102,19 +163,12 @@
::deleted-list-ids
(fn [db _]
(let [kind-5-events (filter (fn [e] (= 5 (:kind e))) (:events db))
;; TODO find list events after the timestamp of the last kind5 event
;; get d-tags of that events
;; filter that d-tags out of the deleted-list-ids
deleted-list-ids (get-d-ids-from-events kind-5-events)]
(.log js/console "kind-5-events: " (clj->js kind-5-events))
(.log js/console "Deleted list-ids " (clj->js deleted-list-ids))
deleted-list-ids)))
(defn extract-id-from-tags
[s]
(println s)
(let [parts (str/split s #":")]
(if (>= (count parts) 2)
(second parts)
s)))
(re-frame/reg-sub
::missing-events-from-lists
:<- [::events]
@ -123,14 +177,34 @@
(let [event-ids-from-list-tags (->> (into [] cat (map (fn [l] (:tags l)) lists))
(filter #(= (or "a" "e") (first %))) ;; just a and e tags
(map second) ;; just the id
(map extract-id-from-tags)
(map nostr/extract-id-from-tag)
(set))
event-ids (set (map #(:id %) events))
missing-events (set/difference event-ids-from-list-tags event-ids)]
(.log js/console "Missing IDs: " (clj->js missing-events))
; (.log js/console "Missing IDs: " (clj->js missing-events))
missing-events)))
(comment
(>= 2 (count (str/split "3013:fjkldj:https://jfdajdfklö" #":")))
(re-frame/reg-sub
::show-lists-modal
(fn [db _]
(:show-lists-modal db)))
(extract-id-from-tags "30142:e2d8b8e3381386976a57091199d23:https://wtcs.pressbooks.pub/digitalliteracy/"))
(re-frame/reg-sub
::show-create-list-modal
(fn [db _]
(:show-create-list-modal db)))
(re-frame/reg-sub
::show-event-data-modal
(fn [db _]
(:show-event-data-modal db)))
(re-frame/reg-sub
::selected-event
(fn [db _]
(:selected-event db)))
(re-frame/reg-sub
::events-in-list
(fn [db [_ event-ids]]
(filter (fn [e] (contains? event-ids (:id e))) (:events db))))

View file

@ -1,9 +1,11 @@
(ns ied.views
(:require
[re-frame.core :as re-frame]
[cljs.pprint :refer [pprint]]
[ied.events :as events]
[ied.routes :as routes]
[ied.subs :as subs]
[ied.nostr :as nostr]
[reagent.core :as reagent]))
;; add resource form
@ -47,31 +49,121 @@
:on-click #(re-frame/dispatch [::events/convert-amb-and-publish-as-nostr-event (:json-string @s)])}
"Publish as Nostr Event"]])))
(defn add-resosurce-by-uri []
(let [uri (reagent/atom {:uri ""})]
(fn []
[:form {:on-submit (fn [e] (.preventDefault e))}
[:label {:for "uri"} "URI: "]
[:input {:id "uri"
:on-change (fn [e]
(swap! uri assoc :uri (-> e .-target .-value)))}]
[:button {:class "btn btn-warning"
:on-click #(re-frame/dispatch [::events/publish-amb-uri-as-nostr-event (:uri @uri)])}
"Publish as Nostr Event"]])))
;; event data modal
(defn event-data-modal []
(let [visible? @(re-frame/subscribe [::subs/show-event-data-modal])
selected-event @(re-frame/subscribe [::subs/selected-event])]
(when visible?
[:dialog {:open visible? :class "modal"}
[:div {:class "modal-box relative flex flex-col"}
[:h3 {:class "text-lg font-bold"} (nostr/get-name-from-metadata-event selected-event)]
[:pre (with-out-str (pprint selected-event))]
[:p {:class "py-4"} "Press ESC key or click outside to close"]]
[:form {:on-click #(re-frame/dispatch [::events/toggle-show-event-data-modal])
:method "dialog" :class "modal-backdrop"}
[:button "close"]]])))
;; metadata event component
(defn metadata-event-component [event]
(let [selected-events @(re-frame/subscribe [::subs/selected-events])]
[:div
{:class "card bg-base-100 w-96 shadow-xl min-h-[620px]"}
[:figure
[:img
{:class "h-48 object-cover"
:src
(nostr/get-image-from-metadata-event event)
:alt ""}]]
[:div
{:class "card-body"}
[:a {:href (nostr/get-d-id-from-event event)
:class "card-title hover:underline"}
(nostr/get-name-from-metadata-event event)]
(doall
(for [about (nostr/get-about-names-from-metadata-event event)]
[:div {:class "badge badge-primary m-1 truncate "
:key about} about]))
[:p {:class "break-all"}
(nostr/get-description-from-metadata-event event)]
[:button {:on-click #(re-frame/dispatch [::events/toggle-show-event-data-modal event])} "Show Event Data"]
[:div
{:class "card-actions justify-end"}
[:div
{:class "form-control"}
[:label
{:class "cursor-pointer label"}
[:span {:class "label-text"} ""]
[:input
{:type "checkbox"
:checked (contains? (set (map #(:id %) selected-events)) (:id event))
:class "checkbox checkbox-success"
:on-change #(re-frame/dispatch [::events/toggle-selected-events event])}]]]]]]))
;; events
(defn events-panel []
(let [events (re-frame/subscribe [::subs/events])
(let [events @(re-frame/subscribe [::subs/metadata-events])
selected-events @(re-frame/subscribe [::subs/selected-events])
show-add-event (re-frame/subscribe [::subs/show-add-event])]
[:div {:class "border-2 rounded"}
[:p {:on-click #(re-frame/dispatch [::events/toggle-show-add-event])}
(if @show-add-event "X" "Add Resource!")]
(when @show-add-event
[add-resource-form])
[:p (str "Num of events: " (count @events))]
(if (> (count @events) 0)
(doall
(for [event @events]
[:li {:key (:id event)} (get event :content "")
[:input {:type "checkbox"
:on-click #(re-frame/dispatch [::events/toggle-selected-events event])}]]))
[:p (str "Num of events: " (count events))]
(if (> (count events) 0)
[:div {:class "flex flex-wrap justify-center gap-2"}
(doall
(for [event events]
[:div {:key (:id event)}
[metadata-event-component event]]))]
[:p "no events there"])
[:button {:class "btn"
:disabled (not (boolean (seq selected-events)))
:on-click #(re-frame/dispatch [::events/add-resources-to-list [{:d "unique-id-1"
:name "Test List SC"}
selected-events]])}
:on-click #(re-frame/dispatch [::events/add-metadata-event-to-list [{:d "unique-id-1"
:name "Test List SC"}
selected-events]])}
"Add To Lists"]]))
;; event feed component
(defn event-feed-component [event]
(let [_ () #_(re-frame/dispatch [::events/add-confetti])]
[:div
{:class "animate-flyIn card bg-base-100 w-64 h-64 shadow-xl border border-white border-w "}
[:p (:kind event)]
[:figure
[:img
{:class "h-48 object-contain"
:src
(nostr/get-image-from-metadata-event event)
:alt ""}]]
[:div
{:class "card-body"}
[:button {:on-click #(re-frame/dispatch [::events/toggle-show-event-data-modal event])} "Show Event Data"]]]))
(defn event-feed-panel []
(let [events @(re-frame/subscribe [::subs/feed-events])]
[:div {:class ""}
[:h1 "Event Feed"]
[:p (str "Num of events: " (count events))]
(if (> (count events) 0)
[:div {:class "flex flex-row gap-2"}
(doall
(for [event events]
[:div {:key (:id event)}
[event-feed-component event]]))]
[:p "no events there"])]))
(defmethod routes/panels :event-feed-panel [] [event-feed-panel])
;; relays
(defn add-relay-form
[name uri]
@ -95,7 +187,6 @@
[:button {:on-click #(re-frame/dispatch [::events/create-websocket {:name (:name @s)
:id (random-uuid)
:uri (:uri @s)}])}
"Add Relay"]])))
(defn relays-panel
@ -126,34 +217,198 @@
;(re-frame/dispatch [::events/connect-to-default-relays])
)]))
;; Header
;; checkmark
(defn checkmark []
[:svg
{:version "1.1",
:class "fa-icon ml-auto mr-2 svelte-1mc5hvj",
:width "16",
:height "16",
:aria-label "",
:role "presentation",
:viewBox "0 0 1792 1792",
; :style "color: black;"
}
[:path
{:d
"M1671 566q0 40-28 68l-724 724-136 136q-28 28-68 28t-68-28l-136-136-362-362q-28-28-28-68t28-68l136-136q28-28 68-28t68 28l294 295 656-657q28-28 68-28t68 28l136 136q28 28 28 68z"}]])
(defn header []
(let [pk (re-frame/subscribe [::subs/pk])]
;; Add to lists modal
(defn create-list-modal []
(let [name (reagent/atom "")
visible? @(re-frame/subscribe [::subs/show-create-list-modal])]
(when visible?
[:dialog {:open visible? :class "modal"}
[:div {:class "modal-box relative flex flex-col"}
[:h3 {:class "text-lg font-bold"} "Hello!"]
[:p {:class "py-4"} "Press ESC key or click outside to close"]
[:input
{:type "text"
:on-change (fn [e] (reset! name (-> e .-target .-value)))
:placeholder "List Name"
:class "input input-bordered w-full max-w-xs"}]
[:button {:class "btn"
:on-click #(re-frame/dispatch [::events/create-new-list @name])} "Create New List"]]
[:form {:on-click #(re-frame/dispatch [::events/toggle-show-create-list-modal])
:method "dialog" :class "modal-backdrop"}
[:button "close"]]])))
(defn add-to-lists-modal []
(let [selected-list-ids @(re-frame/subscribe [::subs/selected-list-ids])
selected-lists @(re-frame/subscribe [::subs/selected-lists])
selected-metadata-events @(re-frame/subscribe [::subs/selected-events])
visible? @(re-frame/subscribe [::subs/show-lists-modal])
lists @(re-frame/subscribe [::subs/lists-of-user])
sorted-lists (nostr/sort-lists lists)]
(when visible?
[:dialog {:open visible? :class "modal"}
[:div {:class "modal-box relative flex flex-col"}
[:h3 {:class "text-lg font-bold"}
"Hello!"]
[:p {:class "py-4"}
"Press ESC key or click outside to close"]
(doall
(for [l lists]
(let [in-selected-lists (or (and (seq selected-metadata-events)
(every? (fn [e] (nostr/list-contains-metadata-event? l e)) selected-metadata-events))
(contains? selected-list-ids (:id l)))
div-class (str "m-2 flex flex-row items-center rounded border border-solid border-white p-2 "
(if in-selected-lists
"bg-green-500 text-black"
"hover:bg-orange-500 hover:text-black"))]
[:div {:class div-class
:key (nostr/get-list-name l)
:on-click #(re-frame/dispatch [::events/toggle-selected-list-ids (:id l)])}
[:p {:class "text-xl font-bold"}
(nostr/get-list-name l)]
(when in-selected-lists
[checkmark])])))
[:div {:class "flex flex-row"}
[:button {:on-click #(re-frame/dispatch [::events/add-metadata-events-to-lists [selected-metadata-events selected-lists]])
:class "btn"}
"Add Resources To Lists"]
[:button {:class "btn ml-auto mr-0"
:on-click #(re-frame/dispatch [::events/toggle-show-create-list-modal])}
"Create New List"]]]
[:form {:on-click #(re-frame/dispatch [::events/toggle-show-lists-modal])
:method "dialog"
:class "modal-backdrop"}
[:button
"close"]]])))
;; shopping cart
(defn shopping-cart []
(let [selected-events @(re-frame/subscribe [::subs/selected-events])]
[:div {:class "dropdown dropdown-end"}
[:div
{:tabIndex "0", :role "button", :class "btn btn-ghost btn-circle"}
[:div
{:class "indicator"}
[:svg
{:xmlns "http://www.w3.org/2000/svg",
:class "h-5 w-5",
:fill "none",
:viewBox "0 0 24 24",
:stroke "currentColor"}
[:path
{:stroke-linecap "round",
:stroke-linejoin "round",
:stroke-width "2",
:d
"M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"}]]
[:span {:class "badge badge-sm indicator-item"} (count selected-events)]]]
[:div
{:tabIndex "0",
:class
"card card-compact dropdown-content bg-base-100 z-[1] mt-3 w-52 shadow"}
[:div
{:class "card-body"}
[:span {:class "text-lg font-bold"}
(str (count selected-events)
" Items")]
[:div {:class "card-actions"}
[:button {:class "btn"
:on-click #(re-frame/dispatch [::events/toggle-show-lists-modal])}
"Add To Lists"]]]]]))
(defn user-menu []
(let [pk @(re-frame/subscribe [::subs/pk])]
(if pk
[:div
{:class "dropdown dropdown-end"}
[:div
{:tabIndex "0",
:role "button",
:class "btn btn-ghost btn-circle avatar"}
[:div
{:class "w-10 rounded-full"}
[:img
{:alt "Tailwind CSS Navbar component",
:src
"https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.webp"}]]]
[:ul
{:tabIndex "0",
:class
"menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow"}
[:li
[:a
{:on-click #(re-frame/dispatch [::events/navigate [:npub-view :npub (nostr/get-npub-from-pk pk)]])}
"Profile"]]
[:li [:a {:on-click #(re-frame/dispatch [::events/navigate [:settings]])} "Settings"]]
[:li [:a {:on-click #(re-frame/dispatch [::events/navigate [:keys]])} "Keys"]]
[:li [:a {:on-click #(re-frame/dispatch [::events/logout])} "Logout"]]]]
[:div
{:class "dropdown dropdown-end"}
[:div {:tabIndex "0", :role "button", :class "btn m-1"} "Login"]
[:ul
{:tabIndex "0",
:class
"dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow"}
[:li [:a {:on-click #(re-frame/dispatch [::events/create-sk])}
"... Anonymously"]]
[:li [:a {:on-click #(re-frame/dispatch [::events/login-with-extension])}
"... with Extension"]]]])))
;; Header
(defn new-header []
(let [selected-events @(re-frame/subscribe [::subs/selected-events])]
[:div
[:a {:on-click #(re-frame/dispatch [::events/navigate :home])}
"home"]
"|"
[:a {:on-click #(re-frame/dispatch [::events/navigate :add-resource])}
"add resource"]
"|"
[:a {:on-click #(re-frame/dispatch [::events/navigate :about])}
"about"]
"|"
[:a {:on-click #(re-frame/dispatch [::events/navigate :settings])}
"settings"]
"|"
(if @pk
[:a {:on-click #(re-frame/dispatch [::events/logout])} "logout"]
[:a {:on-click #(re-frame/dispatch [::events/login-with-extension])} "login"])]))
{:class "navbar bg-base-100"}
[:div
{:class "flex-1"}
[:a {:class "btn btn-ghost text-xl"
:on-click #(re-frame/dispatch [::events/navigate [:home]])}
"edufeed"]
[:a {:class "btn btn-circle"
:on-click #(re-frame/dispatch [::events/navigate [:event-feed]])} "Event-Feed"]]
[:div
{:class "flex-none"}
[:a {:class "btn btn-circle"
:on-click #(re-frame/dispatch [::events/navigate [:add-resource]])} "+"]
[shopping-cart]
[user-menu]]]))
;; Keys Panel
(defn keys-panel []
(let [npub @(re-frame/subscribe [::subs/npub])
nsec @(re-frame/subscribe [::subs/nsec])] [:div
[:h1 "Keys"]
[:p (str "Your Npub: " npub)]
[:p (str "Your Nsec: " nsec)]]))
(defmethod routes/panels :keys-panel [] [keys-panel])
;; Add Resource Panel
(defn add-resource-panel []
[:div
[:h1 "Add Resource"]
[add-resource-form]
[add-resource-by-json]])
[add-resource-by-json]
[add-resosurce-by-uri]])
(defmethod routes/panels :add-resource-panel [] [add-resource-panel])
@ -167,12 +422,8 @@
;; Home
(defn home-panel []
(let [name (re-frame/subscribe [::subs/name])
events (re-frame/subscribe [::subs/events])]
(let [events (re-frame/subscribe [::subs/events])]
[:div
[:h1
(str "Hello from " @name ". This is the Home Page.")]
[events-panel]
[:p (count @events)]]))
@ -183,39 +434,56 @@
[:div
[:h1 "This is the About Page."]
[:div
[:a {:on-click #(re-frame/dispatch [::events/navigate :home])}
[:a {:on-click #(re-frame/dispatch [::events/navigate [:home]])}
"go to Home Page"]]])
(defmethod routes/panels :about-panel [] [about-panel])
;; npub
;; Lists
(defn list-component [list]
(let [pk @(re-frame/subscribe [::subs/pk])
ids-in-list (nostr/get-event-ids-from-list list)
events-in-list @(re-frame/subscribe [::subs/events-in-list ids-in-list]) ;; TODO should be sorted after appeareande in tags
]
[:div {:key (:id list)
:class "p-2"}
[:details
{:class "collapse bg-base-200"}
[:summary
{:class "collapse-title text-xl font-medium"}
(nostr/get-list-name list)]
[:div {:class "collapse-content"}
(doall
(for [event events-in-list]
[:div {:key (:id event)}
[metadata-event-component event]
(when pk
[:button {:class "btn btn-warning"
:on-click #(re-frame/dispatch [::events/delete-event-from-list [event list]])}
"Delete Resource from List"])]))
(when pk [:button {:class "btn btn-error"
:on-click #(re-frame/dispatch [::events/delete-list list])} "Delete List"])]]]))
;; npub / Profile
(defn npub-view-panel []
(let [sockets @(re-frame/subscribe [::subs/sockets])
route-params @(re-frame/subscribe [::subs/route-params])
lists @(re-frame/subscribe [::subs/lists])
lists @(re-frame/subscribe [::subs/lists-for-npub])
_ (when (not (seq lists)) (re-frame/dispatch [::events/get-lists-for-npub (:npub route-params)]))
missing-events-from-lists @(re-frame/subscribe [::subs/missing-events-from-lists])
_ (when (seq missing-events-from-lists) (re-frame/dispatch [::events/query-for-event-ids [sockets missing-events-from-lists]]))]
[:div
[:h1 (str "Hello Npub: " (:npub route-params))]
(if-not (seq lists)
[:p "No lists there...yet"]
[:div {:class "p-2"} "Got some lists"
(doall
(for [l lists]
[:div {:class "p-2"
:key (:id l)}
[:li
[:p (str "ID: " (:id l))]
[:p (str "Name: " (first (filter #(= "d" (first %)) (:tags l))))]
;; TODO filter tags for already being in events
;; for all that are not send a query
[:p (str "Tags: " (:tags l))]
[:button {:class "btn btn-error"
:on-click #(re-frame/dispatch [::events/delete-list l])} "Delete List"]]]))])
[:button {:class "btn"
:on-click
#(re-frame/dispatch [::events/get-lists-for-npub [sockets (:npub route-params)]])} "Load lists"]]))
(doall
(for [l lists]
^{:key (:id l)} [list-component l])))
[:button {:class "btn ml-auto mr-0"
:on-click #(re-frame/dispatch [::events/toggle-show-create-list-modal])}
"Create New List"]]))
(defmethod routes/panels :npub-view-panel [] [npub-view-panel])
@ -223,10 +491,10 @@
(defn main-panel []
(let [active-panel (re-frame/subscribe [::subs/active-panel])]
[:div
[header]
[new-header]
[:div
(routes/panels @active-panel)]]))
(routes/panels @active-panel)
[add-to-lists-modal]
[create-list-modal]
[event-data-modal]]]))
(comment
(.log js/console "Hello World"))

View file

@ -2,7 +2,18 @@
module.exports = {
content: ["./src/**/*.{html,js,cljs}"],
theme: {
extend: {},
extend: {
keyframes: {
flyIn: {
'0%': { transform: 'translateX(-100%)', opacity: '0' },
'100%': { transform: 'translateX(0)', opacity: '1' },
},
},
animation: {
flyIn: 'flyIn 0.5s ease-out forwards',
},
},
},
plugins: [require("daisyui")],
}