diff --git a/project.clj b/project.clj index 800bd252..a3e30035 100644 --- a/project.clj +++ b/project.clj @@ -39,7 +39,7 @@ [district0x/district-time "1.0.1"] [district0x/district-ui-component-active-account "1.0.1"] [district0x/district-ui-component-active-account-balance "1.0.1"] - [district0x/district-ui-component-form "0.2.11"] + [district0x/district-ui-component-form "0.2.14"] [district0x/district-ui-component-meta-tags "1.0.0"] [district0x/district-ui-component-notification "1.0.0"] [district0x/district-ui-component-tx-button "1.0.0"] diff --git a/resources/public/assets/icons/play-button.svg b/resources/public/assets/icons/play-button.svg new file mode 100644 index 00000000..aec93557 --- /dev/null +++ b/resources/public/assets/icons/play-button.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/memefactory/server/db.cljs b/src/memefactory/server/db.cljs index ed1fd856..3618c63d 100644 --- a/src/memefactory/server/db.cljs +++ b/src/memefactory/server/db.cljs @@ -48,6 +48,7 @@ [:meme/number :integer default-nil] [:meme/image-hash ipfs-hash not-nil] [:meme/meta-hash ipfs-hash not-nil] + [:meme/animation-hash ipfs-hash default-nil] [:meme/total-supply :unsigned :integer not-nil] [:meme/total-minted :unsigned :integer not-nil] [:meme/token-id-start :varchar default-nil] @@ -247,7 +248,10 @@ (defn patch-forbidden-reg-entry-image! [address] (db/run! {:update :memes :set {:meme/image-hash "forbidden-image-hash"} - :where [:= :reg-entry/address address]})) + :where [:= :reg-entry/address address]}) + (db/run! {:update :memes + :set {:meme/animation-hash "forbidden-animation-hash"} + :where [:and [:= :reg-entry/address address] [:<> :meme/animation-hash nil]]})) (defn assign-meme-number! [address n] (db/run! {:update :memes diff --git a/src/memefactory/server/emailer.cljs b/src/memefactory/server/emailer.cljs index 391b54ea..9569b394 100644 --- a/src/memefactory/server/emailer.cljs +++ b/src/memefactory/server/emailer.cljs @@ -61,7 +61,7 @@ (defn send-challenge-created-email [{:keys [:registry-entry :commit-period-end] :as ev}] (safe-go - (let [{:keys [:reg-entry/creator :meme/title :meme/image-hash] :as meme} (db/get-meme registry-entry) + (let [{:keys [:reg-entry/creator :meme/title :meme/image-hash :meme/animation-hash] :as meme} (db/get-meme registry-entry) {:keys [:from :template-id :api-key :print-mode?]} (get-in @config/config [:emailer]) root-url (format/ensure-trailing-slash (get-in @config/config [:ui :root-url])) ipfs-gateway-url (format/ensure-trailing-slash (get-in @config/config [:ipfs :gateway])) @@ -69,7 +69,7 @@ [unit value] (time/time-remaining-biggest-unit (cljs-time/now) (-> commit-period-end time/epoch->long time-coerce/from-long)) time-remaining (format/format-time-units {unit value}) - meme-image-url (str ipfs-gateway-url image-hash) + meme-image-url (str ipfs-gateway-url (or animation-hash image-hash)) email ( {:name title +(defn is-video? [file-info] + (= "video/mp4" (:type file-info))) + + +(defn build-meme-meta-string [{:keys [title search-tags issuance comment file-info]} image-or-video-hash thumbnail-hash] + (-> (merge {:name title :description (string/trim (or comment "")) :external_url "https://memefactory.io/" - :image (str "ipfs://" image-hash) + :image (str "ipfs://" (if (is-video? file-info) + thumbnail-hash + image-or-video-hash)) :attributes { :search-tags search-tags :issuance issuance}} + (when (is-video? file-info) {:animation_url (str "ipfs://" image-or-video-hash)})) clj->js js/JSON.stringify)) (re-frame/reg-event-fx ::upload-meme - (fn [_ [_ {:keys [:form-data :deposit] :as data}]] - (log/info "Uploading meme image" {:file (:file-info form-data)} ::upload-meme) - {:ipfs/call {:func "add" - :args [(:file (:file-info form-data))] - :opts {:wrap-with-directory true} - :on-success [::upload-meme-meta data] - :on-error [::logging/error "upload-meme ipfs call error" {:data form-data} - ::upload-meme]}})) + (fn [_ [_ {:keys [:form-data] :as data}]] + (if (is-video? (:file-info form-data)) + {:dispatch [::upload-thumbnail data]} + {:dispatch [::upload-meme-image data nil]}))) + + +(re-frame/reg-event-fx + ::upload-thumbnail + (fn [_ [_ {:keys [:form-data] :as data}]] + (log/info "Uploading video thumbnail" {:file (:video-thumbnail form-data)} ::upload-thumbnail) + {:ipfs/call {:func "add" + :args [(:video-thumbnail form-data)] + :opts {:wrap-with-directory true} + :on-success [::upload-meme-image data] + :on-error [::logging/error "upload-thumbnail ipfs call error" {:data form-data} + ::upload-meme]}})) + + +(defn get-thumbnail-hash [ipfs-thumbnail-response] + (let [resp (utils/parse-ipfs-response ipfs-thumbnail-response) + thumbnail-hash (str (-> resp last :Hash) "/" (-> resp first :Name))] + thumbnail-hash)) + +(re-frame/reg-event-fx + ::upload-meme-image + (fn [_ [_ {:keys [:form-data :deposit] :as data} ipfs-thumbnail-response]] + (let [thumbnail-hash (when ipfs-thumbnail-response + (log/info "Video thumbnail uploaded" {:ipfs-response ipfs-thumbnail-response} ::upload-meme-image) + (get-thumbnail-hash ipfs-thumbnail-response))] + (log/info "Uploading meme image" {:file (:file-info form-data)} ::upload-meme) + {:ipfs/call {:func "add" + :args [(:file (:file-info form-data))] + :opts {:wrap-with-directory true} + :on-success [::upload-meme-meta (merge data {:thumbnail-hash thumbnail-hash})] + :on-error [::logging/error "upload-meme ipfs call error" {:data form-data} + ::upload-meme]}}))) (re-frame/reg-event-fx ::upload-meme-meta - (fn [{:keys [db]} [_ data ipfs-response]] + (fn [{:keys [db]} [_ {:keys [:form-data :thumbnail-hash] :as data} ipfs-response]] (log/info "Meme image uploaded" {:ipfs-response ipfs-response} ::upload-meme-meta) (try-catch (let [resp (utils/parse-ipfs-response ipfs-response) - image-hash (str (-> resp last :Hash) "/" (-> resp first :Name)) - meme-meta (build-meme-meta-string (:form-data data) image-hash) + image-or-video-hash (str (-> resp last :Hash) "/" (-> resp first :Name)) + meme-meta (build-meme-meta-string form-data image-or-video-hash thumbnail-hash) buffer-data (js/buffer.Buffer.from meme-meta)] (log/info "Uploading meme meta" {:meme-meta meme-meta} ::upload-meme-meta) {:ipfs/call {:func "add" :args [buffer-data] :on-success [::meme-factory/approve-and-create-meme data] :on-error [::logging/error "upload-meme-meta ipfs call error" - {:data (:form-data data) + {:data form-data :meme-meta meme-meta} ::upload-meme-meta]}})))) diff --git a/src/memefactory/ui/dank_registry/submit_page.cljs b/src/memefactory/ui/dank_registry/submit_page.cljs index 6e716bf1..93e23e1e 100644 --- a/src/memefactory/ui/dank_registry/submit_page.cljs +++ b/src/memefactory/ui/dank_registry/submit_page.cljs @@ -101,16 +101,35 @@ :id :file-info :errors errors :label "Upload a file" - :comment "Upload image with ratio 2:3 and size less than 1.5MB" + :comment "Upload image with ratio 2:3 and size less than 25MB" + :video-attributes {:controls true + :autoPlay false + :on-loadedData (fn [e] + (let [video (.-target e)] + ;; sets time to trigger on-seeked, to make sure video is rendered before building thumbnail + (set! (.-currentTime video) 0))) + :on-seeked (fn [e] + ;; builds video thumbnail right after video is loaded and rendered + (let [video (.-target e)] + (def canvas (.getElementById js/document "thumbnail-id")) + (set! (.-width canvas) (.-videoWidth video)) + (set! (.-height canvas) (.-videoHeight video)) + (def ctx (.getContext canvas "2d")) + (.drawImage ctx video 0 0) + (.toBlob canvas (fn [blob] + (swap! form-data assoc :video-thumbnail + (js/File. [blob] (str (-> @form-data :file-info :file .-name) "-thumbnail.png")))))))} :file-accept-pred (fn [{:keys [name type size] :as props}] - (log/debug "Veryfing acceptance of file" {:name name :type type :size size}) + (log/debug "Verifying acceptance of file" {:name name :type type :size size}) (and (#{"image/png" "image/gif" "image/jpeg" "image/svg+xml" "video/mp4"} type) - (< size 1500000))) + (< size 25000000))) :on-file-accepted (fn [{:keys [name type size array-buffer] :as props}] (swap! form-data update-in [:file-info] dissoc :error) + (swap! form-data dissoc :video-thumbnail) (log/info "Accepted file" {:name name :type type :size size} ::file-accepted)) :on-file-rejected (fn [{:keys [name type size] :as props}] (swap! form-data assoc :file-info {:error "Non .png .jpeg .gif .svg or .mp4 file selected with size less than 1.5 Mb"}) + (swap! form-data dissoc :video-thumbnail) (log/warn "Rejected file" {:name name :type type :size size :user {:id active-account}} ::file-rejected))}]] [:div.form-panel [with-label "Title" @@ -168,7 +187,8 @@ "Submit")] [dank-with-logo (web3/from-wei deposit-value :ether)]] (when (< @account-balance deposit-value) - [:div.not-enough-dank "You don't have enough DANK tokens to submit a meme"])]]]]))) + [:div.not-enough-dank "You don't have enough DANK tokens to submit a meme"]) + [:canvas {:hidden true :id "thumbnail-id"}]]]]]))) (defmethod page :route.dank-registry/submit [] diff --git a/src/memefactory/ui/home/page.cljs b/src/memefactory/ui/home/page.cljs index 07db69d3..305744cb 100644 --- a/src/memefactory/ui/home/page.cljs +++ b/src/memefactory/ui/home/page.cljs @@ -38,12 +38,12 @@ :show-cards-left? true} auc])))]))])) -(defn trending-vote-tile [{:keys [:reg-entry/address :meme/image-hash :reg-entry/creator :challenge/commit-period-end +(defn trending-vote-tile [{:keys [:reg-entry/address :meme/image-hash :meme/animation-hash :reg-entry/creator :challenge/commit-period-end :challenge/challenger :challenge/comment] :as meme}] (let [{:keys [:user/total-created-challenges-success :user/total-created-challenges]} challenger] [:div.compact-tile - [tiles/flippable-tile {:front [tiles/meme-image image-hash] + [tiles/flippable-tile {:front [tiles/meme-image image-hash animation-hash] :back [:div.meme-card.back [:div.overlay @@ -111,6 +111,7 @@ [:reg-entry/address :meme/title :meme/image-hash + :meme/animation-hash :meme/total-minted :meme/number]]]]]) @@ -159,6 +160,7 @@ :user/total-created-challenges]] :meme/title :meme/image-hash + :meme/animation-hash :challenge/votes-total :challenge/commit-period-end :challenge/comment]]]]) diff --git a/src/memefactory/ui/leaderboard/dankest_page.cljs b/src/memefactory/ui/leaderboard/dankest_page.cljs index 60e6bfd8..738a8e90 100644 --- a/src/memefactory/ui/leaderboard/dankest_page.cljs +++ b/src/memefactory/ui/leaderboard/dankest_page.cljs @@ -35,6 +35,7 @@ [:items [:reg-entry/address [:reg-entry/creator [:user/address :user/creator-rank]] :meme/image-hash + :meme/animation-hash :reg-entry/created-on :meme/title :meme/number diff --git a/src/memefactory/ui/marketplace/page.cljs b/src/memefactory/ui/marketplace/page.cljs index 554a2339..3a9216c9 100644 --- a/src/memefactory/ui/marketplace/page.cljs +++ b/src/memefactory/ui/marketplace/page.cljs @@ -68,6 +68,7 @@ [:meme/title :reg-entry/address :meme/image-hash + :meme/animation-hash :meme/number :meme/total-minted]]]]]]]]) diff --git a/src/memefactory/ui/meme_detail/page.cljs b/src/memefactory/ui/meme_detail/page.cljs index bc16905d..24c7fa54 100644 --- a/src/memefactory/ui/meme_detail/page.cljs +++ b/src/memefactory/ui/meme_detail/page.cljs @@ -50,6 +50,7 @@ :reg-entry/created-on :reg-entry/challenge-period-end :meme/image-hash + :meme/animation-hash :meme/meta-hash :meme/number :meme/title @@ -147,6 +148,7 @@ :meme/title :meme/number :meme/image-hash + :meme/animation-hash :meme/meta-hash :meme/total-minted]]]]]]]]])] (fn [address tags] @@ -553,7 +555,7 @@ :meme (shared-utils/reg-entry-dates-to-seconds))] (assoc meme :reg-entry/status (shared-utils/reg-entry-status @now (shared-utils/reg-entry-dates-to-seconds meme))))) - {:keys [:reg-entry/status :meme/image-hash :meme/title :meme/number :reg-entry/status :meme/total-supply :reg-entry/created-on + {:keys [:reg-entry/status :meme/image-hash :meme/animation-hash :meme/title :meme/number :reg-entry/status :meme/total-supply :reg-entry/created-on :meme/tags :meme/owned-meme-tokens :reg-entry/creator :challenge/challenger :reg-entry/challenge-period-end :challenge/reveal-period-end]} meme token-count (->> owned-meme-tokens (map :meme-token/token-id) @@ -584,7 +586,7 @@ (when number [:div.meme-number {:key :meme-number} (str "#" number)]) ^{:key :container} - [tiles/meme-image image-hash {:rejected? (-> (gql-utils/gql-name->kw status) (= :reg-entry.status/blacklisted))}] + [tiles/meme-image image-hash animation-hash {:rejected? (-> (gql-utils/gql-name->kw status) (= :reg-entry.status/blacklisted))}] (if exists? [:div.registry {:key :registry} [share-buttons/share-buttons (. (. js/document -location) -href)] diff --git a/src/memefactory/ui/memefolio/page.cljs b/src/memefactory/ui/memefolio/page.cljs index c5fb8ffe..5701bc0d 100644 --- a/src/memefactory/ui/memefolio/page.cljs +++ b/src/memefactory/ui/memefolio/page.cljs @@ -314,7 +314,7 @@ :has-more? has-more? :load-fn re-search} (when-not loading-first? - (doall (map (fn [{:keys [:reg-entry/address :meme/image-hash :meme/number + (doall (map (fn [{:keys [:reg-entry/address :meme/image-hash :meme/animation-hash :meme/number :meme/title :meme/total-supply :meme/owned-meme-tokens] :as meme}] (when address (let [token-ids (map :meme-token/token-id owned-meme-tokens) @@ -323,7 +323,7 @@ count) active-user-page? (or (empty? url-address) (= url-address active-account))] [:div.compact-tile {:key address} - [tiles/flippable-tile {:front [tiles/meme-image image-hash + [tiles/flippable-tile {:front [tiles/meme-image image-hash animation-hash {:class "collected-tile-front"}] :back (if active-user-page? [collected-tile-back {:meme/number number @@ -361,6 +361,7 @@ :reg-entry/address :meme/number :meme/image-hash + :meme/animation-hash :meme/total-minted]]]]]]]] [:search-memes [:total-count]] [:search-meme-tokens [:total-count]]]}]) @@ -452,14 +453,14 @@ [:div.spinner-container [spinner/spin]]) :load-fn re-search} (when-not loading-first? - (doall (map (fn [{:keys [:reg-entry/address :meme/image-hash :meme/number + (doall (map (fn [{:keys [:reg-entry/address :meme/image-hash :meme/animation-hash :meme/number :meme/title :meme/total-supply :meme/total-minted] :as meme}] (when address (let [status (shared-utils/reg-entry-status @now (shared-utils/reg-entry-dates-to-seconds meme)) rejected? (= status :reg-entry.status/blacklisted)] ^{:key address} [:div.compact-tile [:div.container - [tiles/meme-image image-hash {:rejected? rejected?}]] + [tiles/meme-image image-hash animation-hash {:rejected? rejected?}]] [nav-anchor {:route :route.meme-detail/index :params {:address address} :query nil @@ -554,7 +555,7 @@ :load-fn re-search} (when-not loading-first? (doall - (map (fn [{:keys [:reg-entry/address :meme/image-hash :meme/number + (map (fn [{:keys [:reg-entry/address :meme/image-hash :meme/animation-hash :meme/number :meme/title :challenge/vote] :as meme}] (when address (let [{:keys [:vote/option]} vote @@ -565,7 +566,7 @@ [:div.compact-tile [:div.container [:div.meme-card-front - [tiles/meme-image image-hash {:rejected? rejected?}]]] + [tiles/meme-image image-hash animation-hash {:rejected? rejected?}]]] [nav-anchor {:route :route.meme-detail/index :params {:address address} :query nil @@ -687,7 +688,7 @@ {:keys [:meme/title :meme/total-minted] :as meme} :meme-token/meme :as meme-token} :meme-auction/meme-token :as meme-auction}] [:div.compact-tile {:key address} - [tiles/flippable-tile {:front [tiles/meme-image (get-in meme-token [:meme-token/meme :meme/image-hash])] + [tiles/flippable-tile {:front [tiles/meme-image (get-in meme-token [:meme-token/meme :meme/image-hash]) (get-in meme-token [:meme-token/meme :meme/animation-hash])] :back [:div.meme-card [:div.overlay @@ -747,6 +748,7 @@ [:items (remove nil? [:reg-entry/address :reg-entry/created-on :meme/image-hash + :meme/animation-hash :meme/meta-hash :meme/number :meme/title @@ -775,6 +777,7 @@ :has-next-page [:items [:reg-entry/address :meme/image-hash + :meme/animation-hash :meme/meta-hash :meme/number :meme/title @@ -808,6 +811,7 @@ :has-next-page [:items [:reg-entry/address :meme/image-hash + :meme/animation-hash :meme/meta-hash :meme/number :meme/title @@ -849,6 +853,7 @@ :meme/title :meme/number :meme/image-hash + :meme/animation-hash :meme/meta-hash :meme/total-minted]]]]]]]]] :sold [[:search-meme-auctions (merge {:seller user-address @@ -879,6 +884,7 @@ [:reg-entry/address :meme/title :meme/image-hash + :meme/animation-hash :meme/meta-hash :meme/number :meme/total-minted]]]]]]]]])))