mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
@@ -84,6 +84,7 @@ example. It's still usable as before, we just removed the example.
|
||||
- Add new shape validation mechanism for shapes [Github #7696](https://github.com/penpot/penpot/pull/7696)
|
||||
- Apply color tokens from sidebar [Taiga #11353](https://tree.taiga.io/project/penpot/us/11353)
|
||||
- Display tokens in the inspect tab [Taiga #9313](https://tree.taiga.io/project/penpot/us/9313)
|
||||
- Refactor clipboard behavior to assess some minor inconsistencies and make pasting binary data faster. [Taiga #12571](https://tree.taiga.io/project/penpot/task/12571)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
@@ -98,6 +99,7 @@ example. It's still usable as before, we just removed the example.
|
||||
- Fix shortcut conflict in text editor (increase/decrease font size vs word selection)
|
||||
- Fix problem with plugins generating code for pages different than current one [Taiga #12312](https://tree.taiga.io/project/penpot/issue/12312)
|
||||
- Fix input confirmation behavior is not uniform [Taiga #12294](https://tree.taiga.io/project/penpot/issue/12294)
|
||||
- Fix copy/pasting application/transit+json [Taiga #12721](https://tree.taiga.io/project/penpot/issue/12721)
|
||||
|
||||
## 2.11.1
|
||||
|
||||
|
||||
@@ -1679,7 +1679,7 @@ Will return a value that matches this schema:
|
||||
["id" {:optional true} :string]
|
||||
["name" :string]
|
||||
["description" :string]
|
||||
["isSource" :boolean]
|
||||
["isSource" {:optional true} :boolean]
|
||||
["selectedTokenSets"
|
||||
[:map-of :string [:enum "enabled" "disabled"]]]]]]
|
||||
["$metadata" {:optional true}
|
||||
|
||||
@@ -115,6 +115,11 @@ services:
|
||||
volumes:
|
||||
- "valkey_data:/data"
|
||||
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- redis
|
||||
|
||||
mailer:
|
||||
image: sj26/mailcatcher:latest
|
||||
restart: always
|
||||
@@ -123,6 +128,12 @@ services:
|
||||
ports:
|
||||
- "1080:1080"
|
||||
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- mailer
|
||||
|
||||
|
||||
# https://github.com/rroemhild/docker-test-openldap
|
||||
ldap:
|
||||
image: rroemhild/test-openldap:2.1
|
||||
@@ -136,3 +147,9 @@ services:
|
||||
nofile:
|
||||
soft: 1024
|
||||
hard: 1024
|
||||
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- ldap
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: 3.07. Abstraction levels
|
||||
desc: "Penpot Technical Guide: organize data and logic in clear abstraction layers—ADTs, file ops, event-sourced changes, business rules, and data events."
|
||||
---
|
||||
|
||||
# Code organization in abstraction levels
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: 3.06. Backend Guide
|
||||
desc: "Penpot Technical Guide: Backend basics - REPL setup, loading fixtures, database migrations, and clj-kondo linting to speed development workflows."
|
||||
---
|
||||
|
||||
# Backend guide #
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: 1.2 Install with Elestio
|
||||
desc: "Step-by-step guide to deploy a self-hosted Penpot on Elestio: 3-minute setup, managed DNS/SMTP/SSL/backups, Docker Compose config, updates & support."
|
||||
---
|
||||
|
||||
# Install with Elestio
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: Design Tokens
|
||||
order: 5
|
||||
desc: Learn how to create, manage and apply Penpot Design Tokens using W3C DTCG format, with sets, themes, aliases, equations and JSON import/export.
|
||||
---
|
||||
|
||||
<h1 id="design-tokens">Design Tokens</h1>
|
||||
|
||||
@@ -195,7 +195,9 @@
|
||||
(ptk/reify ::login-from-token
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rx/of (logged-in (with-meta profile {::ev/source "login-with-token"})))
|
||||
(->> (dp/on-fetch-profile-success profile)
|
||||
(rx/map (fn [profile]
|
||||
(logged-in (with-meta profile {::ev/source "login-with-token"}))))
|
||||
;; NOTE: we need this to be asynchronous because the effect
|
||||
;; should be called before proceed with the login process
|
||||
(rx/observe-on :async)))))
|
||||
|
||||
@@ -77,22 +77,25 @@
|
||||
(rx/of (rt/nav-raw :href href)))
|
||||
(rx/throw cause))))
|
||||
|
||||
(defn on-fetch-profile-success
|
||||
[profile]
|
||||
(if (and (contains? cf/flags :subscriptions)
|
||||
(is-authenticated? profile))
|
||||
(->> (rp/cmd! :get-subscription-usage {})
|
||||
(rx/map (fn [{:keys [editors]}]
|
||||
(update-in profile [:props :subscription] assoc :editors editors)))
|
||||
(rx/catch (fn [cause]
|
||||
(js/console.error "unexpected error on obtaining subscription usage" cause)
|
||||
(rx/of profile))))
|
||||
(rx/of profile)))
|
||||
|
||||
(defn fetch-profile
|
||||
[]
|
||||
(ptk/reify ::fetch-profile
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :get-profile)
|
||||
(rx/mapcat (fn [profile]
|
||||
(if (and (contains? cf/flags :subscriptions)
|
||||
(is-authenticated? profile))
|
||||
(->> (rp/cmd! :get-subscription-usage {})
|
||||
(rx/map (fn [{:keys [editors]}]
|
||||
(update-in profile [:props :subscription] assoc :editors editors)))
|
||||
(rx/catch (fn [cause]
|
||||
(js/console.error "unexpected error on obtaining subscription usage" cause)
|
||||
(rx/of profile))))
|
||||
(rx/of profile))))
|
||||
(rx/mapcat on-fetch-profile-success)
|
||||
(rx/map (partial ptk/data-event ::profile-fetched))
|
||||
(rx/catch on-fetch-profile-exception)))))
|
||||
|
||||
|
||||
@@ -254,6 +254,10 @@
|
||||
(declare ^:private paste-svg-text)
|
||||
(declare ^:private paste-shapes)
|
||||
|
||||
(def ^:private default-options
|
||||
#js {:decodeTransit t/decode-str
|
||||
:allowHTMLPaste (features/active-feature? @st/state "text-editor/v2-html-paste")})
|
||||
|
||||
(defn create-paste-from-blob
|
||||
[in-viewport?]
|
||||
(fn [blob]
|
||||
@@ -290,7 +294,7 @@
|
||||
(ptk/reify ::paste-from-clipboard
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (clipboard/from-navigator)
|
||||
(->> (clipboard/from-navigator default-options)
|
||||
(rx/mapcat default-paste-from-blob)
|
||||
(rx/take 1)))))
|
||||
|
||||
@@ -308,7 +312,7 @@
|
||||
;; we forbid that scenario so the default behaviour is executed
|
||||
(if is-editing?
|
||||
(rx/empty)
|
||||
(->> (clipboard/from-synthetic-clipboard-event event)
|
||||
(->> (clipboard/from-synthetic-clipboard-event event default-options)
|
||||
(rx/mapcat (create-paste-from-blob in-viewport?))))))))
|
||||
|
||||
(defn copy-selected-svg
|
||||
@@ -478,7 +482,7 @@
|
||||
(js/console.error "Clipboard error:" cause))
|
||||
(rx/empty)))]
|
||||
|
||||
(->> (clipboard/from-navigator)
|
||||
(->> (clipboard/from-navigator default-options)
|
||||
(rx/mapcat #(.text %))
|
||||
(rx/map decode-entry)
|
||||
(rx/take 1)
|
||||
|
||||
@@ -60,10 +60,10 @@
|
||||
(defn render-thumbnail
|
||||
[file-id revn]
|
||||
(if (features/active-feature? @st/state "render-wasm/v1")
|
||||
(->> (mw/ask! {:cmd :thumbnails/generate-for-file-wasm
|
||||
:revn revn
|
||||
:file-id file-id
|
||||
:width thumbnail-width}))
|
||||
(mw/ask! {:cmd :thumbnails/generate-for-file-wasm
|
||||
:revn revn
|
||||
:file-id file-id
|
||||
:width thumbnail-width})
|
||||
(->> (mw/ask! {:cmd :thumbnails/generate-for-file
|
||||
:revn revn
|
||||
:file-id file-id
|
||||
|
||||
@@ -83,74 +83,101 @@
|
||||
::mf/register-as :management-dialog}
|
||||
[{:keys [subscription-type current-subscription editors subscribe-to-trial]}]
|
||||
|
||||
(let [unlimited-modal-step* (mf/use-state 1)
|
||||
unlimited-modal-step (deref unlimited-modal-step*)
|
||||
subscription-name (if subscribe-to-trial
|
||||
(if (= subscription-type "unlimited")
|
||||
(tr "subscription.settings.unlimited-trial")
|
||||
(tr "subscription.settings.enterprise-trial"))
|
||||
(case subscription-type
|
||||
"professional" (tr "subscription.settings.professional")
|
||||
"unlimited" (tr "subscription.settings.unlimited")
|
||||
"enterprise" (tr "subscription.settings.enterprise")))
|
||||
min-editors (if (seq editors) (count editors) 1)
|
||||
initial (mf/with-memo [min-editors]
|
||||
{:min-members min-editors})
|
||||
form (fm/use-form :schema (schema:seats-form min-editors)
|
||||
:initial initial)
|
||||
submit-in-progress* (mf/use-state false)
|
||||
subscribe-to-unlimited (mf/use-fn
|
||||
(mf/deps form)
|
||||
(fn [add-payment-details]
|
||||
(when (not @submit-in-progress*)
|
||||
(let [data (:clean-data @form)
|
||||
return-url (-> (rt/get-current-href) (rt/encode-url))
|
||||
href (if add-payment-details
|
||||
(dm/str "payments/subscriptions/create?type=unlimited&show=true&quantity=" (:min-members data) "&returnUrl=" return-url)
|
||||
(dm/str "payments/subscriptions/create?type=unlimited&show=false&quantity=" (:min-members data) "&returnUrl=" return-url))]
|
||||
(reset! submit-in-progress* true)
|
||||
(reset! form nil)
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription"
|
||||
:type "unlimited"
|
||||
:quantity (:min-members data)})
|
||||
(rt/nav-raw :href href))))))
|
||||
(let [unlimited-modal-step*
|
||||
(mf/use-state 1)
|
||||
|
||||
subscribe-to-enterprise (mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription"
|
||||
:type "enterprise"}))
|
||||
(let [return-url (-> (rt/get-current-href) (rt/encode-url))
|
||||
href (dm/str "payments/subscriptions/create?type=enterprise&returnUrl=" return-url)]
|
||||
(st/emit! (rt/nav-raw :href href)))))
|
||||
unlimited-modal-step
|
||||
(deref unlimited-modal-step*)
|
||||
|
||||
handle-accept-dialog (mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management"
|
||||
::ev/origin "settings"
|
||||
:section "subscription-management-modal"}))
|
||||
(let [current-href (rt/get-current-href)
|
||||
returnUrl (js/encodeURIComponent current-href)
|
||||
href (dm/str "payments/subscriptions/show?returnUrl=" returnUrl)]
|
||||
(st/emit! (rt/nav-raw :href href)))
|
||||
(modal/hide!)))
|
||||
handle-close-dialog (mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "close-subscription-modal"}))
|
||||
(modal/hide!)))
|
||||
subscription-name
|
||||
(if subscribe-to-trial
|
||||
(if (= subscription-type "unlimited")
|
||||
(tr "subscription.settings.unlimited-trial")
|
||||
(tr "subscription.settings.enterprise-trial"))
|
||||
(case subscription-type
|
||||
"professional" (tr "subscription.settings.professional")
|
||||
"unlimited" (tr "subscription.settings.unlimited")
|
||||
"enterprise" (tr "subscription.settings.enterprise")))
|
||||
|
||||
handle-unlimited-modal-step (mf/use-fn
|
||||
(mf/deps unlimited-modal-step)
|
||||
(fn []
|
||||
(if (= unlimited-modal-step 1)
|
||||
(reset! unlimited-modal-step* 2)
|
||||
(reset! unlimited-modal-step* 1))))
|
||||
min-editors
|
||||
(if (seq editors) (count editors) 1)
|
||||
|
||||
show-editors-list* (mf/use-state false)
|
||||
show-editors-list (deref show-editors-list*)
|
||||
handle-click (mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(swap! show-editors-list* not)))]
|
||||
initial
|
||||
(mf/with-memo [min-editors]
|
||||
{:min-members min-editors})
|
||||
|
||||
form
|
||||
(fm/use-form :schema (schema:seats-form min-editors)
|
||||
:initial initial)
|
||||
|
||||
form-data-min-editors
|
||||
(-> @form :clean-data :min-members)
|
||||
|
||||
submit-in-progress*
|
||||
(mf/use-state false)
|
||||
|
||||
subscribe-to-unlimited
|
||||
(mf/use-fn
|
||||
(mf/deps form-data-min-editors)
|
||||
(fn [add-payment-details]
|
||||
(when (not @submit-in-progress*)
|
||||
(let [return-url (-> (rt/get-current-href) (rt/encode-url))
|
||||
href (if add-payment-details
|
||||
(dm/str "payments/subscriptions/create?type=unlimited&show=true&quantity=" form-data-min-editors "&returnUrl=" return-url)
|
||||
(dm/str "payments/subscriptions/create?type=unlimited&show=false&quantity=" form-data-min-editors "&returnUrl=" return-url))]
|
||||
(reset! submit-in-progress* true)
|
||||
(reset! form nil)
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription"
|
||||
:type "unlimited"
|
||||
:quantity form-data-min-editors})
|
||||
(rt/nav-raw :href href))))))
|
||||
|
||||
subscribe-to-enterprise
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription"
|
||||
:type "enterprise"}))
|
||||
(let [return-url (-> (rt/get-current-href) (rt/encode-url))
|
||||
href (dm/str "payments/subscriptions/create?type=enterprise&returnUrl=" return-url)]
|
||||
(st/emit! (rt/nav-raw :href href)))))
|
||||
|
||||
handle-accept-dialog
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management"
|
||||
::ev/origin "settings"
|
||||
:section "subscription-management-modal"}))
|
||||
(let [current-href (rt/get-current-href)
|
||||
returnUrl (js/encodeURIComponent current-href)
|
||||
href (dm/str "payments/subscriptions/show?returnUrl=" returnUrl)]
|
||||
(st/emit! (rt/nav-raw :href href)))
|
||||
(modal/hide!)))
|
||||
|
||||
handle-close-dialog
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "close-subscription-modal"}))
|
||||
(modal/hide!)))
|
||||
|
||||
handle-unlimited-modal-step
|
||||
(mf/use-fn
|
||||
(mf/deps unlimited-modal-step)
|
||||
(fn []
|
||||
(if (= unlimited-modal-step 1)
|
||||
(reset! unlimited-modal-step* 2)
|
||||
(reset! unlimited-modal-step* 1))))
|
||||
|
||||
show-editors-list*
|
||||
(mf/use-state false)
|
||||
|
||||
show-editors-list
|
||||
(deref show-editors-list*)
|
||||
|
||||
handle-click
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(swap! show-editors-list* not)))]
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-dialog)}
|
||||
@@ -246,7 +273,7 @@
|
||||
[:input
|
||||
{:class (stl/css :cancel-button)
|
||||
:type "button"
|
||||
:value (tr "ubscription.settings.management-dialog.step-2-skip-button")
|
||||
:value (tr "subscription.settings.management-dialog.step-2-skip-button")
|
||||
:on-click #(subscribe-to-unlimited false)}]
|
||||
|
||||
[:input
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
@use "ds/typography.scss" as t;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/spacing.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
|
||||
.dashboard-section {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
inline-size: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -20,10 +22,10 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
max-width: deprecated.$s-500;
|
||||
max-inline-size: $sz-500;
|
||||
margin-block-end: var(--sp-xxxl);
|
||||
width: deprecated.$s-580;
|
||||
margin: deprecated.$s-92 auto deprecated.$s-120 auto;
|
||||
inline-size: px2rem(580);
|
||||
margin: px2rem(92) auto px2rem(120) auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -98,8 +100,8 @@
|
||||
.plan-title-icon {
|
||||
@extend .button-icon;
|
||||
stroke: var(--color-foreground-primary);
|
||||
height: var(--sp-xl);
|
||||
width: var(--sp-xl);
|
||||
block-size: var(--sp-xl);
|
||||
inline-size: var(--sp-xl);
|
||||
border-radius: 6px;
|
||||
border: 1.75px solid var(--color-foreground-primary);
|
||||
stroke-width: 2.25px;
|
||||
@@ -140,7 +142,7 @@
|
||||
}
|
||||
|
||||
.other-subscriptions {
|
||||
margin-block-start: deprecated.$s-52;
|
||||
margin-block-start: px2rem(52);
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
@@ -155,8 +157,8 @@
|
||||
|
||||
.cta-button svg {
|
||||
@extend .button-icon;
|
||||
height: var(--sp-l);
|
||||
width: var(--sp-l);
|
||||
block-size: var(--sp-l);
|
||||
inline-size: var(--sp-l);
|
||||
stroke: var(--color-accent-primary);
|
||||
margin-inline-start: var(--sp-xs);
|
||||
}
|
||||
@@ -179,14 +181,12 @@
|
||||
|
||||
.modal-dialog {
|
||||
@extend .modal-container-base;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
max-height: initial;
|
||||
min-width: deprecated.$s-548;
|
||||
max-block-size: initial;
|
||||
min-inline-size: px2rem(548);
|
||||
}
|
||||
|
||||
.modal-dialog.subscription-success {
|
||||
min-width: deprecated.$s-648;
|
||||
min-inline-size: px2rem(648);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
@@ -232,11 +232,11 @@
|
||||
|
||||
.modal-success-content {
|
||||
display: flex;
|
||||
gap: deprecated.$s-40;
|
||||
gap: $sz-40;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
margin-block-start: deprecated.$s-40;
|
||||
margin-block-start: $sz-40;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
@@ -249,23 +249,28 @@
|
||||
|
||||
.primary-button {
|
||||
@extend .modal-accept-btn;
|
||||
min-block-size: $sz-32;
|
||||
block-size: auto;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@extend .modal-cancel-btn;
|
||||
min-block-size: $sz-32;
|
||||
white-space: break-spaces;
|
||||
block-size: auto;
|
||||
}
|
||||
|
||||
.modal-start {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width: deprecated.$s-220;
|
||||
max-inline-size: $sz-224;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
inline-size: 100%;
|
||||
block-size: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
@media (max-inline-size: 992px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -285,19 +290,19 @@
|
||||
list-style-position: inside;
|
||||
list-style-type: none;
|
||||
margin-inline-start: var(--sp-xl);
|
||||
max-height: deprecated.$s-216;
|
||||
max-block-size: px2rem(216);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
--input-icon-padding: var(--sp-s);
|
||||
width: deprecated.$s-80;
|
||||
inline-size: px2rem(80);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
@include t.use-typography("body-small");
|
||||
color: var(--color-foreground-error);
|
||||
margin-block-start: deprecated.$s-8;
|
||||
margin-block-start: var(--sp-s);
|
||||
}
|
||||
|
||||
.editors-wrapper {
|
||||
@@ -316,7 +321,7 @@
|
||||
@include t.use-typography("body-small");
|
||||
background-color: var(--color-background-tertiary);
|
||||
border-radius: var(--sp-s);
|
||||
margin-block-start: deprecated.$s-40;
|
||||
margin-block-start: $sz-40;
|
||||
padding-block: var(--sp-s);
|
||||
padding-inline: var(--sp-m);
|
||||
}
|
||||
|
||||
@@ -128,11 +128,12 @@
|
||||
|
||||
(defn update-text-rect!
|
||||
[id]
|
||||
(mw/emit!
|
||||
{:cmd :index/update-text-rect
|
||||
:page-id (:current-page-id @st/state)
|
||||
:shape-id id
|
||||
:dimensions (get-text-dimensions id)}))
|
||||
(when wasm/context-initialized?
|
||||
(mw/emit!
|
||||
{:cmd :index/update-text-rect
|
||||
:page-id (:current-page-id @st/state)
|
||||
:shape-id id
|
||||
:dimensions (get-text-dimensions id)})))
|
||||
|
||||
|
||||
(defn- ensure-text-content
|
||||
@@ -198,70 +199,71 @@
|
||||
(defn set-shape-children
|
||||
[children]
|
||||
(perf/begin-measure "set-shape-children")
|
||||
(case (count children)
|
||||
0
|
||||
(h/call wasm/internal-module "_set_children_0")
|
||||
(let [children (into [] (filter uuid?) children)]
|
||||
(case (count children)
|
||||
0
|
||||
(h/call wasm/internal-module "_set_children_0")
|
||||
|
||||
1
|
||||
(let [[c1] children
|
||||
c1 (uuid/get-u32 c1)]
|
||||
(h/call wasm/internal-module "_set_children_1"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)))
|
||||
1
|
||||
(let [[c1] children
|
||||
c1 (uuid/get-u32 c1)]
|
||||
(h/call wasm/internal-module "_set_children_1"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)))
|
||||
|
||||
2
|
||||
(let [[c1 c2] children
|
||||
c1 (uuid/get-u32 c1)
|
||||
c2 (uuid/get-u32 c2)]
|
||||
(h/call wasm/internal-module "_set_children_2"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
|
||||
(aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)))
|
||||
2
|
||||
(let [[c1 c2] children
|
||||
c1 (uuid/get-u32 c1)
|
||||
c2 (uuid/get-u32 c2)]
|
||||
(h/call wasm/internal-module "_set_children_2"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
|
||||
(aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)))
|
||||
|
||||
3
|
||||
(let [[c1 c2 c3] children
|
||||
c1 (uuid/get-u32 c1)
|
||||
c2 (uuid/get-u32 c2)
|
||||
c3 (uuid/get-u32 c3)]
|
||||
(h/call wasm/internal-module "_set_children_3"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
|
||||
(aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
|
||||
(aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)))
|
||||
3
|
||||
(let [[c1 c2 c3] children
|
||||
c1 (uuid/get-u32 c1)
|
||||
c2 (uuid/get-u32 c2)
|
||||
c3 (uuid/get-u32 c3)]
|
||||
(h/call wasm/internal-module "_set_children_3"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
|
||||
(aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
|
||||
(aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)))
|
||||
|
||||
4
|
||||
(let [[c1 c2 c3 c4] children
|
||||
c1 (uuid/get-u32 c1)
|
||||
c2 (uuid/get-u32 c2)
|
||||
c3 (uuid/get-u32 c3)
|
||||
c4 (uuid/get-u32 c4)]
|
||||
(h/call wasm/internal-module "_set_children_4"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
|
||||
(aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
|
||||
(aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)
|
||||
(aget c4 0) (aget c4 1) (aget c4 2) (aget c4 3)))
|
||||
4
|
||||
(let [[c1 c2 c3 c4] children
|
||||
c1 (uuid/get-u32 c1)
|
||||
c2 (uuid/get-u32 c2)
|
||||
c3 (uuid/get-u32 c3)
|
||||
c4 (uuid/get-u32 c4)]
|
||||
(h/call wasm/internal-module "_set_children_4"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
|
||||
(aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
|
||||
(aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)
|
||||
(aget c4 0) (aget c4 1) (aget c4 2) (aget c4 3)))
|
||||
|
||||
5
|
||||
(let [[c1 c2 c3 c4 c5] children
|
||||
c1 (uuid/get-u32 c1)
|
||||
c2 (uuid/get-u32 c2)
|
||||
c3 (uuid/get-u32 c3)
|
||||
c4 (uuid/get-u32 c4)
|
||||
c5 (uuid/get-u32 c5)]
|
||||
(h/call wasm/internal-module "_set_children_5"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
|
||||
(aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
|
||||
(aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)
|
||||
(aget c4 0) (aget c4 1) (aget c4 2) (aget c4 3)
|
||||
(aget c5 0) (aget c5 1) (aget c5 2) (aget c5 3)))
|
||||
5
|
||||
(let [[c1 c2 c3 c4 c5] children
|
||||
c1 (uuid/get-u32 c1)
|
||||
c2 (uuid/get-u32 c2)
|
||||
c3 (uuid/get-u32 c3)
|
||||
c4 (uuid/get-u32 c4)
|
||||
c5 (uuid/get-u32 c5)]
|
||||
(h/call wasm/internal-module "_set_children_5"
|
||||
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
|
||||
(aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
|
||||
(aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)
|
||||
(aget c4 0) (aget c4 1) (aget c4 2) (aget c4 3)
|
||||
(aget c5 0) (aget c5 1) (aget c5 2) (aget c5 3)))
|
||||
|
||||
;; Dynamic call for children > 5
|
||||
(let [heap (mem/get-heap-u32)
|
||||
size (mem/get-alloc-size children UUID-U8-SIZE)
|
||||
offset (mem/alloc->offset-32 size)]
|
||||
(reduce
|
||||
(fn [offset id]
|
||||
(mem.h32/write-uuid offset heap id))
|
||||
offset
|
||||
children)
|
||||
(h/call wasm/internal-module "_set_children")))
|
||||
;; Dynamic call for children > 5
|
||||
(let [heap (mem/get-heap-u32)
|
||||
size (mem/get-alloc-size children UUID-U8-SIZE)
|
||||
offset (mem/alloc->offset-32 size)]
|
||||
(reduce
|
||||
(fn [offset id]
|
||||
(mem.h32/write-uuid offset heap id))
|
||||
offset
|
||||
children)
|
||||
(h/call wasm/internal-module "_set_children"))))
|
||||
(perf/end-measure "set-shape-children")
|
||||
nil)
|
||||
|
||||
@@ -468,14 +470,14 @@
|
||||
[attrs]
|
||||
(let [style (:style attrs)
|
||||
;; Filter to only supported attributes
|
||||
allowed-keys #{:fill :fillRule :strokeLinecap :strokeLinejoin}
|
||||
allowed-keys #{:fill :fillRule :fill-rule :strokeLinecap :stroke-linecap :strokeLinejoin :stroke-linejoin}
|
||||
attrs (-> attrs
|
||||
(dissoc :style)
|
||||
(merge style)
|
||||
(select-keys allowed-keys))
|
||||
fill-rule (-> attrs :fillRule sr/translate-fill-rule)
|
||||
stroke-linecap (-> attrs :strokeLinecap sr/translate-stroke-linecap)
|
||||
stroke-linejoin (-> attrs :strokeLinejoin sr/translate-stroke-linejoin)
|
||||
fill-rule (or (-> attrs :fill-rule sr/translate-fill-rule) (-> attrs :fillRule sr/translate-fill-rule))
|
||||
stroke-linecap (or (-> attrs :stroke-linecap sr/translate-stroke-linecap) (-> attrs :strokeLinecap sr/translate-stroke-linecap))
|
||||
stroke-linejoin (or (-> attrs :stroke-linejoin sr/translate-stroke-linejoin) (-> attrs :strokeLinejoin sr/translate-stroke-linejoin))
|
||||
fill-none (= "none" (-> attrs :fill))]
|
||||
(h/call wasm/internal-module "_set_shape_svg_attrs" fill-rule stroke-linecap stroke-linejoin fill-none)))
|
||||
|
||||
@@ -739,7 +741,7 @@
|
||||
|
||||
(d/nilv align-self 0)
|
||||
is-absolute
|
||||
(d/nilv z-index))))
|
||||
(d/nilv z-index 0))))
|
||||
|
||||
(defn clear-layout
|
||||
[]
|
||||
@@ -1031,8 +1033,9 @@
|
||||
(into full-acc full)))
|
||||
{:thumbnails thumbnails-acc :full full-acc}))]
|
||||
(perf/end-measure "set-objects")
|
||||
(process-pending shapes thumbnails full render-callback
|
||||
(process-pending shapes thumbnails full noop-fn
|
||||
(fn []
|
||||
(when render-callback (render-callback))
|
||||
(ug/dispatch! (ug/event "penpot:wasm:set-objects")))))))
|
||||
|
||||
(defn clear-focus-mode
|
||||
|
||||
@@ -83,12 +83,13 @@
|
||||
|
||||
(defn update-text-layout
|
||||
[id]
|
||||
(let [shape-id-buffer (uuid/get-u32 id)]
|
||||
(h/call wasm/internal-module "_update_shape_text_layout_for"
|
||||
(aget shape-id-buffer 0)
|
||||
(aget shape-id-buffer 1)
|
||||
(aget shape-id-buffer 2)
|
||||
(aget shape-id-buffer 3))))
|
||||
(when wasm/context-initialized?
|
||||
(let [shape-id-buffer (uuid/get-u32 id)]
|
||||
(h/call wasm/internal-module "_update_shape_text_layout_for"
|
||||
(aget shape-id-buffer 0)
|
||||
(aget shape-id-buffer 1)
|
||||
(aget shape-id-buffer 2)
|
||||
(aget shape-id-buffer 3)))))
|
||||
|
||||
;; IMPORTANT: Only TTF fonts can be stored.
|
||||
(defn- store-font-buffer
|
||||
|
||||
@@ -18,47 +18,56 @@
|
||||
"image/svg+xml"])
|
||||
|
||||
(def ^:private default-options
|
||||
#js {:decodeTransit t/decode-str})
|
||||
#js {:decodeTransit t/decode-str
|
||||
:allowHTMLPaste false})
|
||||
|
||||
(defn- from-data-transfer
|
||||
"Get clipboard stream from DataTransfer instance"
|
||||
[data-transfer]
|
||||
(->> (rx/from (impl/fromDataTransfer data-transfer default-options))
|
||||
(rx/mapcat #(rx/from %))))
|
||||
([data-transfer]
|
||||
(from-data-transfer data-transfer default-options))
|
||||
([data-transfer options]
|
||||
(->> (rx/from (impl/fromDataTransfer data-transfer options))
|
||||
(rx/mapcat #(rx/from %)))))
|
||||
|
||||
(defn from-navigator
|
||||
[]
|
||||
(->> (rx/from (impl/fromNavigator default-options))
|
||||
(rx/mapcat #(rx/from %))))
|
||||
([]
|
||||
(from-navigator default-options))
|
||||
([options]
|
||||
(->> (rx/from (impl/fromNavigator options))
|
||||
(rx/mapcat #(rx/from %)))))
|
||||
|
||||
(defn from-clipboard-event
|
||||
"Get clipboard stream from clipboard event"
|
||||
[event]
|
||||
(let [cdata (.-clipboardData ^js event)]
|
||||
(from-data-transfer cdata)))
|
||||
([event]
|
||||
(from-clipboard-event event default-options))
|
||||
([event options]
|
||||
(let [cdata (.-clipboardData ^js event)]
|
||||
(from-data-transfer cdata options))))
|
||||
|
||||
(defn from-synthetic-clipboard-event
|
||||
"Get clipboard stream from syntetic clipboard event"
|
||||
[event]
|
||||
(let [target
|
||||
(dom/get-target event)
|
||||
([event options]
|
||||
(let [target
|
||||
(dom/get-target event)
|
||||
|
||||
content-editable?
|
||||
(dom/is-content-editable? target)
|
||||
content-editable?
|
||||
(dom/is-content-editable? target)
|
||||
|
||||
is-input?
|
||||
(= (dom/get-tag-name target) "INPUT")]
|
||||
is-input?
|
||||
(= (dom/get-tag-name target) "INPUT")]
|
||||
|
||||
;; ignore when pasting into an editable control
|
||||
(when-not (or content-editable? is-input?)
|
||||
(-> event
|
||||
(dom/event->browser-event)
|
||||
(from-clipboard-event)))))
|
||||
(when-not (or content-editable? is-input?)
|
||||
(-> event
|
||||
(dom/event->browser-event)
|
||||
(from-clipboard-event options))))))
|
||||
|
||||
(defn from-drop-event
|
||||
"Get clipboard stream from drop event"
|
||||
[event]
|
||||
(from-data-transfer (.-dataTransfer ^js event)))
|
||||
([event]
|
||||
(from-drop-event event default-options))
|
||||
([event options]
|
||||
(from-data-transfer (.-dataTransfer ^js event) options)))
|
||||
|
||||
;; FIXME: rename to `write-text`
|
||||
(defn to-clipboard
|
||||
|
||||
@@ -27,6 +27,7 @@ const exclusiveTypes = [
|
||||
/**
|
||||
* @typedef {Object} ClipboardSettings
|
||||
* @property {Function} [decodeTransit]
|
||||
* @property {boolean} [allowHTMLPaste]
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -38,9 +39,7 @@ const exclusiveTypes = [
|
||||
*/
|
||||
function parseText(text, options) {
|
||||
options = options || {};
|
||||
|
||||
const decodeTransit = options["decodeTransit"];
|
||||
|
||||
if (decodeTransit) {
|
||||
try {
|
||||
decodeTransit(text);
|
||||
@@ -57,18 +56,85 @@ function parseText(text, options) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters ClipboardItem types
|
||||
*
|
||||
* @param {ClipboardSettings} options
|
||||
* @returns {Function<AllowedTypesFilterFunction>}
|
||||
*/
|
||||
function filterAllowedTypes(options) {
|
||||
/**
|
||||
* @param {string} type
|
||||
* @returns {boolean}
|
||||
*/
|
||||
return function filter(type) {
|
||||
if (
|
||||
(!("allowHTMLPaste" in options) || !options["allowHTMLPaste"]) &&
|
||||
type === "text/html"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return allowedTypes.includes(type);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters DataTransferItems
|
||||
*
|
||||
* @param {ClipboardSettings} options
|
||||
* @returns {Function<AllowedTypesFilterFunction>}
|
||||
*/
|
||||
function filterAllowedItems(options) {
|
||||
/**
|
||||
* @param {DataTransferItem}
|
||||
* @returns {boolean}
|
||||
*/
|
||||
return function filter(item) {
|
||||
if (
|
||||
(!("allowHTMLPaste" in options) || !options["allowHTMLPaste"]) &&
|
||||
item.type === "text/html"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return allowedTypes.includes(item.type);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts ClipboardItem types
|
||||
*
|
||||
* @param {string} a
|
||||
* @param {string} b
|
||||
* @returns {number}
|
||||
*/
|
||||
function sortTypes(a, b) {
|
||||
return allowedTypes.indexOf(a) - allowedTypes.indexOf(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts DataTransferItems
|
||||
*
|
||||
* @param {DataTransferItem} a
|
||||
* @param {DataTransferItem} b
|
||||
* @returns {number}
|
||||
*/
|
||||
function sortItems(a, b) {
|
||||
return allowedTypes.indexOf(a.type) - allowedTypes.indexOf(b.type);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ClipboardSettings} [options]
|
||||
* @returns {Promise<Array<Blob>>}
|
||||
*/
|
||||
export async function fromNavigator(options) {
|
||||
options = options || {};
|
||||
const items = await navigator.clipboard.read();
|
||||
return Promise.all(
|
||||
Array.from(items).map(async (item) => {
|
||||
const itemAllowedTypes = Array.from(item.types)
|
||||
.filter((type) => allowedTypes.includes(type))
|
||||
.sort((a, b) => allowedTypes.indexOf(a) - allowedTypes.indexOf(b));
|
||||
.filter(filterAllowedTypes(options))
|
||||
.sort(sortTypes);
|
||||
|
||||
if (
|
||||
itemAllowedTypes.length === 1 &&
|
||||
@@ -96,12 +162,11 @@ export async function fromNavigator(options) {
|
||||
* @returns {Promise<Array<Blob>>}
|
||||
*/
|
||||
export async function fromDataTransfer(dataTransfer, options) {
|
||||
options = options || {};
|
||||
const items = await Promise.all(
|
||||
Array.from(dataTransfer.items)
|
||||
.filter((item) => allowedTypes.includes(item.type))
|
||||
.sort(
|
||||
(a, b) => allowedTypes.indexOf(a.type) - allowedTypes.indexOf(b.type),
|
||||
)
|
||||
.filter(filterAllowedItems(options))
|
||||
.sort(sortItems)
|
||||
.map(async (item) => {
|
||||
if (item.kind === "file") {
|
||||
return Promise.resolve(item.getAsFile());
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.common.logging :as log]
|
||||
[app.common.types.color :as cc]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.render :as render]
|
||||
@@ -125,58 +126,75 @@
|
||||
|
||||
(def thumbnail-aspect-ratio (/ 2 3))
|
||||
|
||||
(defmethod impl/handler :thumbnails/generate-for-file-wasm
|
||||
[{:keys [file-id revn width] :as message} _]
|
||||
(defn render-canvas-blob
|
||||
[canvas width height background-color]
|
||||
(-> (.convertToBlob canvas)
|
||||
(p/then
|
||||
(fn [blob]
|
||||
(rds/renderToStaticMarkup
|
||||
(mf/element
|
||||
svg-wrapper
|
||||
#js {:data-uri (blob->uri blob)
|
||||
:width width
|
||||
:height height
|
||||
:background background-color}))))))
|
||||
|
||||
(defn process-wasm-thumbnail
|
||||
[{:keys [id file-id revn width] :as message}]
|
||||
(->> (rx/from @init-wasm)
|
||||
(rx/mapcat #(request-data-for-thumbnail file-id revn false))
|
||||
(rx/mapcat
|
||||
(fn [{:keys [page] :as file}]
|
||||
(rx/create
|
||||
(fn [subs]
|
||||
(try
|
||||
(let [background-color (or (:background page) cc/canvas)
|
||||
height (* width thumbnail-aspect-ratio)
|
||||
canvas (js/OffscreenCanvas. width height)
|
||||
init? (wasm.api/init-canvas-context canvas)]
|
||||
(if init?
|
||||
(let [objects (:objects page)
|
||||
frame (some->> page :thumbnail-frame-id (get objects))
|
||||
vbox (if frame
|
||||
(-> (gsb/get-object-bounds objects frame)
|
||||
(grc/fix-aspect-ratio thumbnail-aspect-ratio))
|
||||
(render/calculate-dimensions objects thumbnail-aspect-ratio))
|
||||
zoom (/ width (:width vbox))]
|
||||
(let [background-color (or (:background page) cc/canvas)
|
||||
height (* width thumbnail-aspect-ratio)
|
||||
canvas (js/OffscreenCanvas. width height)
|
||||
init? (wasm.api/init-canvas-context canvas)]
|
||||
(if init?
|
||||
(let [objects (:objects page)
|
||||
frame (some->> page :thumbnail-frame-id (get objects))
|
||||
vbox (if frame
|
||||
(-> (gsb/get-object-bounds objects frame)
|
||||
(grc/fix-aspect-ratio thumbnail-aspect-ratio))
|
||||
(render/calculate-dimensions objects thumbnail-aspect-ratio))
|
||||
zoom (/ width (:width vbox))]
|
||||
|
||||
(wasm.api/initialize-viewport
|
||||
objects zoom vbox background-color
|
||||
(fn []
|
||||
(if frame
|
||||
(wasm.api/render-sync-shape (:id frame))
|
||||
(wasm.api/render-sync))
|
||||
(wasm.api/initialize-viewport
|
||||
objects zoom vbox background-color
|
||||
(fn []
|
||||
(if frame
|
||||
(wasm.api/render-sync-shape (:id frame))
|
||||
(wasm.api/render-sync))
|
||||
|
||||
(-> (.convertToBlob canvas)
|
||||
(p/then
|
||||
(fn [blob]
|
||||
(let [data
|
||||
(rds/renderToStaticMarkup
|
||||
(mf/element
|
||||
svg-wrapper
|
||||
#js {:data-uri (blob->uri blob)
|
||||
:width width
|
||||
:height height
|
||||
:background background-color}))]
|
||||
(rx/push! subs {:data data :file-id file-id :revn revn}))))
|
||||
(p/catch #(do (.error js/console %)
|
||||
(rx/error! subs %)))
|
||||
(p/finally #(rx/end! subs))))))
|
||||
(-> (render-canvas-blob canvas width height background-color)
|
||||
(p/then #(rx/push! subs {:id id :data % :file-id file-id :revn revn}))
|
||||
(p/catch #(rx/error! subs %))
|
||||
(p/finally #(rx/end! subs))))))
|
||||
|
||||
(do (rx/error! subs "Error loading webgl context")
|
||||
(rx/end! subs)))
|
||||
(rx/end! subs))
|
||||
|
||||
nil)
|
||||
nil)))))))
|
||||
|
||||
(catch :default err
|
||||
(.error js/console err)
|
||||
(rx/error! subs err)
|
||||
(rx/end! subs)))))))))
|
||||
(defonce thumbs-subject (rx/subject))
|
||||
|
||||
(defonce thumbs-stream
|
||||
(->> thumbs-subject
|
||||
(rx/mapcat process-wasm-thumbnail)
|
||||
(rx/share)))
|
||||
|
||||
(defmethod impl/handler :thumbnails/generate-for-file-wasm
|
||||
[message _]
|
||||
(rx/create
|
||||
(fn [subs]
|
||||
(let [id (uuid/next)
|
||||
sid
|
||||
(->> thumbs-stream
|
||||
(rx/filter #(= id (:id %)))
|
||||
(rx/subs!
|
||||
#(do
|
||||
(rx/push! subs %)
|
||||
(rx/end! subs))))]
|
||||
(rx/push! thumbs-subject (assoc message :id id))
|
||||
|
||||
#(rx/dispose! sid)))))
|
||||
|
||||
@@ -1641,9 +1641,9 @@ impl RenderState {
|
||||
}
|
||||
|
||||
children_ids.sort_by(|id1, id2| {
|
||||
let z1 = tree.get(id1).map_or_else(|| 0, |s| s.z_index());
|
||||
let z2 = tree.get(id2).map_or_else(|| 0, |s| s.z_index());
|
||||
z1.cmp(&z2)
|
||||
let z1 = tree.get(id1).map(|s| s.z_index()).unwrap_or(0);
|
||||
let z2 = tree.get(id2).map(|s| s.z_index()).unwrap_or(0);
|
||||
z2.cmp(&z1)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -332,6 +332,7 @@ fn propagate_reflow(
|
||||
}
|
||||
_ => {
|
||||
// Other shapes don't have to be reflown
|
||||
reflow_parent = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -291,9 +291,10 @@ pub extern "C" fn set_shape_text_content() {
|
||||
let bytes = mem::bytes();
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
let raw_text_data = RawParagraph::try_from(&bytes).unwrap();
|
||||
shape
|
||||
.add_paragraph(raw_text_data.into())
|
||||
.expect("Failed to add paragraph");
|
||||
|
||||
if let Err(_) = shape.add_paragraph(raw_text_data.into()) {
|
||||
println!("Error with set_shape_text_content on {:?}", shape.id);
|
||||
}
|
||||
});
|
||||
mem::free_bytes();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user