2023-02-16 14:29:44 +00:00
|
|
|
# Code Style Guidelines
|
2022-11-14 10:15:49 +00:00
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
> [!IMPORTANT]
|
|
|
|
> The goal of this document is to help all contributors (core and external) to
|
|
|
|
> write code in _unison_ and help establish good practices that serve the Status
|
|
|
|
> Mobile contributors well.
|
2022-11-14 10:15:49 +00:00
|
|
|
|
|
|
|
We don't want to turn this document into an exhaustive list of rules to be
|
|
|
|
followed that nobody will read. As much as possible, we'll try to document only
|
|
|
|
what we consider important for Status Mobile. In other words, we don't want to
|
|
|
|
maintain a general Clojure convention/style guide, nor do we want to turn this
|
|
|
|
document into a long tutorial.
|
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
> [!WARNING]
|
|
|
|
> This is a **work in progress**, and not all conventions are properly
|
|
|
|
> implemented in the codebase yet. The project structure is also undergoing
|
|
|
|
> major changes, and it will take a considerable amount of time until we migrate
|
|
|
|
> the existing code to the new structure.
|
2022-11-14 10:15:49 +00:00
|
|
|
|
|
|
|
If you find out anything is outdated or missing, please, share with us or even
|
|
|
|
better, create a pull-request! 🤸
|
|
|
|
|
|
|
|
## Style guide
|
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
We follow the [Clojure Style
|
|
|
|
Guide](https://github.com/bbatsov/clojure-style-guide) and we use
|
|
|
|
[zprint](https://github.com/kkinnear/zprint) to format Clojure code. Running
|
|
|
|
`make lint-fix` should fix most formatting issues, but not all of them.
|
2022-11-14 10:15:49 +00:00
|
|
|
|
|
|
|
## Dos and don'ts
|
|
|
|
|
2023-04-24 12:38:11 +00:00
|
|
|
### Hiccup
|
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
Never use anonymous inline function in hiccup, this will lead to
|
|
|
|
reinitialization of component on each render of parent component.
|
2023-04-24 12:38:11 +00:00
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
(defn checkbox-view
|
|
|
|
[{:keys [size]}]
|
|
|
|
[rn/view
|
|
|
|
[(fn [] [rn/view])]])
|
|
|
|
|
|
|
|
;; good
|
|
|
|
(defn comp []
|
|
|
|
[rn/view])
|
|
|
|
|
|
|
|
(defn checkbox-view
|
|
|
|
[{:keys [size]}]
|
|
|
|
[rn/view
|
|
|
|
[comp]])
|
|
|
|
```
|
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
This mistake mostly happens with functional components.
|
2023-04-24 12:38:11 +00:00
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
(fn []
|
|
|
|
(let [atom (rf/sub [:sub])]
|
|
|
|
(fn []
|
|
|
|
[:f>
|
|
|
|
(fn []
|
|
|
|
[rn/text atom]
|
|
|
|
|
|
|
|
;; good
|
|
|
|
(defn f-comp [atom]
|
|
|
|
[rn/text atom])
|
2023-08-09 14:32:36 +00:00
|
|
|
|
2023-04-24 12:38:11 +00:00
|
|
|
(fn []
|
|
|
|
(let [atom (rf/sub [:sub])]
|
|
|
|
(fn []
|
2023-08-09 14:32:36 +00:00
|
|
|
[:f> f-comp atom])))
|
2023-04-24 12:38:11 +00:00
|
|
|
```
|
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
It's important to name functional components with `f-` prefix.
|
2023-04-24 12:38:11 +00:00
|
|
|
|
2023-07-10 09:45:39 +00:00
|
|
|
### Component props and API scheme to match Figma as closely as possible
|
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
Ideally, the prop names for components (particularly in quo2 Design System)
|
|
|
|
should match the Figma properties as best as possible. This makes it easier for
|
|
|
|
the developer using that component to configure it correctly for the screen it
|
|
|
|
is being used on and avoids unnecessary overwrites and adjustments being made.
|
2023-07-10 09:45:39 +00:00
|
|
|
|
|
|
|
#### Avoid unnecessarily grouping categories to reduce the number of props
|
2023-08-09 14:32:36 +00:00
|
|
|
|
2023-07-10 09:45:39 +00:00
|
|
|
For example in Figma if there is a component and it has the following variants:
|
|
|
|
|
|
|
|
|theme: "light" blur: "False"|theme: "dark" blur: "False"|theme: "light" blur: "True"|theme: "dark" blur: "True"|
|
|
|
|
|----------------------------|---------------------------|---------------------------|---------------------------|
|
|
|
|
| type :neutral label "ABC" | type :neutral label "ABC" | | |
|
|
|
|
| type :active label "ABC" | type :active label "ABC" | | |
|
|
|
|
| type :danger label "ABC" | type :danger label "ABC" | type :danger label "ABC" | type :danger label "ABC" |
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
"theme - :light or :dark
|
|
|
|
type - can be :neutral :active :danger :danger-blur"
|
|
|
|
(defn my-component [{:keys [theme type]} label])
|
|
|
|
|
|
|
|
;; good
|
|
|
|
"theme - :light or :dark
|
|
|
|
type - can be :neutral :active :danger
|
|
|
|
blur? - boolean
|
|
|
|
"
|
|
|
|
(defn my-component [{:keys [theme blur? type]} label])
|
|
|
|
```
|
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
Please note this is only for the external API of the component and there should
|
|
|
|
be no restriction of how the component manages its internal API as that will not
|
|
|
|
affect the developer using the component with the issues described above.
|
2023-07-10 09:45:39 +00:00
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
In some cases this is not always possible or does not make sense. However the
|
|
|
|
thought process should be how easy will it be for another developer to use this
|
|
|
|
component with the correct configuration given the screen designs for Figma.
|
2023-07-10 09:45:39 +00:00
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
#### Avoid unnecessarily renaming props
|
|
|
|
|
|
|
|
In general it can be helpful to avoid renaming props from their counterpart in
|
|
|
|
Figma.
|
2023-07-10 09:45:39 +00:00
|
|
|
|
|
|
|
For example if Figma has sizes `:small`, `:medium` and `:large`
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
":size - :little, :default or :big"
|
|
|
|
(defn my-component [{:keys [size]}])
|
|
|
|
|
|
|
|
;; good
|
|
|
|
":size - :small, :medium or :large"
|
|
|
|
(defn my-component [{:keys [size]}])
|
|
|
|
```
|
|
|
|
|
2022-11-14 10:15:49 +00:00
|
|
|
### Component styles
|
|
|
|
|
2023-02-02 10:23:06 +00:00
|
|
|
Prefer to define styles in a separate file named `style.cljs`, colocated with
|
|
|
|
the source file. For a real example, see
|
|
|
|
[src/quo2/components/record_audio/record_audio/style.cljs](../src/quo2/components/record_audio/record_audio/style.cljs).
|
2022-11-14 10:15:49 +00:00
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
2023-02-02 10:23:06 +00:00
|
|
|
(defn checkbox-view
|
|
|
|
[{:keys [size]}]
|
|
|
|
[rn/view
|
2022-11-14 10:15:49 +00:00
|
|
|
{:style {:width size
|
|
|
|
:height size
|
|
|
|
:border-radius 4
|
|
|
|
:justify-content :center
|
|
|
|
:align-items :center}}
|
2023-02-02 10:23:06 +00:00
|
|
|
[rn/view (do-something)]])
|
2022-11-14 10:15:49 +00:00
|
|
|
|
|
|
|
;; good
|
2023-02-02 10:23:06 +00:00
|
|
|
(defn checkbox-view
|
|
|
|
[{:keys [size]}]
|
|
|
|
[rn/view {:style (style/checkbox size)}
|
|
|
|
[rn/view (do-something)]])
|
2022-11-14 10:15:49 +00:00
|
|
|
```
|
|
|
|
|
2023-03-27 20:06:09 +00:00
|
|
|
### Always add styles inside the `:style` key
|
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
Although when compiling ReactNative for mobile some components are able work with
|
2023-03-27 20:06:09 +00:00
|
|
|
their styles in the top-level of the properties map, prefer to add them inside the
|
|
|
|
`:style` key in order to separate styles from properties:
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
[rn/button {:flex 1
|
|
|
|
:padding-vertical 10
|
|
|
|
:padding-horizontal 20
|
|
|
|
:on-press #(js/alert "Hi!")
|
|
|
|
:title "Button"}]
|
|
|
|
|
|
|
|
;; good
|
|
|
|
[rn/button {:style {:flex 1
|
|
|
|
:padding-vertical 10
|
|
|
|
:padding-horizontal 20}
|
|
|
|
:on-press #(js/alert "Hi!")
|
|
|
|
:title "Button"}]
|
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
;; better
|
2023-03-27 20:06:09 +00:00
|
|
|
;; (define them in a style ns & place them inside `:style` key)
|
|
|
|
[rn/button {:style (style/button)
|
|
|
|
:on-press #(js/alert "Hi!")
|
2023-08-09 14:32:36 +00:00
|
|
|
:title "Button"}]
|
2023-03-27 20:06:09 +00:00
|
|
|
```
|
|
|
|
|
2023-04-24 12:38:11 +00:00
|
|
|
Also its fine to keep one liner styles in view
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; ok
|
|
|
|
[rn/view {:style {:flex 1 :padding-top 5}}]
|
|
|
|
```
|
|
|
|
|
|
|
|
### Don't define properties in styles ns
|
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
Properties must be set on view level
|
2023-04-24 12:38:11 +00:00
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
{:style {:position :absolute
|
|
|
|
:left 0
|
|
|
|
:right 0
|
|
|
|
:bottom 0}
|
|
|
|
:blur-amount 30
|
|
|
|
:blur-radius 25
|
|
|
|
:blur-type :transparent
|
|
|
|
:overlay-color :transparent}
|
|
|
|
|
|
|
|
;; good
|
|
|
|
{:position :absolute
|
|
|
|
:left 0
|
|
|
|
:right 0
|
|
|
|
:bottom 0}
|
|
|
|
```
|
|
|
|
|
|
|
|
### Apply animated styles in the style file
|
2023-03-28 14:29:54 +00:00
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
(defn circle
|
|
|
|
[]
|
|
|
|
(let [opacity (reanimated/use-shared-value 1)]
|
|
|
|
[reanimated/view {:style (reanimated/apply-animations-to-style
|
|
|
|
{:opacity opacity}
|
|
|
|
style/circle-container)}]))
|
|
|
|
|
|
|
|
;; good
|
|
|
|
(defn circle
|
|
|
|
[]
|
|
|
|
(let [opacity (reanimated/use-shared-value 1)]
|
|
|
|
[reanimated/view {:style (style/circle-container opacity)}]))
|
|
|
|
```
|
|
|
|
|
2023-01-31 12:06:29 +00:00
|
|
|
### Don't use percents to define width/height
|
|
|
|
|
2023-02-07 13:05:38 +00:00
|
|
|
In ReactNative, all layouts use the [flexbox
|
|
|
|
model](https://reactnative.dev/docs/flexbox), so percentages are unnecessary the
|
|
|
|
vast majority of the time, don't use them. Check out this great [interactive
|
|
|
|
flexbox guide](https://www.joshwcomeau.com/css/interactive-guide-to-flexbox/) by
|
|
|
|
Joshua Comeau.
|
2023-01-31 12:06:29 +00:00
|
|
|
|
2023-02-07 13:05:38 +00:00
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
[rn/view {:style {:width "80%"}}]
|
2023-01-31 12:06:29 +00:00
|
|
|
|
2023-02-07 13:05:38 +00:00
|
|
|
;; good
|
|
|
|
[rn/view {:style {:padding-horizontal 20}}]
|
|
|
|
```
|
2023-01-31 12:06:29 +00:00
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
### Use a question mark to convey the value is a boolean
|
2023-02-16 14:29:44 +00:00
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
The Clojure Style Guide suggests using a question mark only in [predicate
|
|
|
|
functions](https://guide.clojure.style/#naming-predicates), but nothing is
|
|
|
|
mentioned about other symbols and keywords. We prefer to extend the convention
|
|
|
|
to all boolean references.
|
2023-02-16 14:29:44 +00:00
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
(let [is-open? true] ...)
|
2023-08-09 14:32:36 +00:00
|
|
|
(def flag-is-enabled false)
|
2023-02-16 14:29:44 +00:00
|
|
|
|
|
|
|
;; good
|
|
|
|
(let [open? true] ...)
|
2023-08-09 14:32:36 +00:00
|
|
|
(def flag-enabled? false)
|
|
|
|
```
|
|
|
|
|
|
|
|
And for keywords too:
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
[some-component {:logged-in true}]
|
|
|
|
|
|
|
|
;; good
|
|
|
|
[some-component {:logged-in? true}]
|
2023-02-16 14:29:44 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
### Styles def vs defn
|
2022-11-28 12:58:36 +00:00
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
Always use `def` over `defn` if there are no dynamic values. This helps cut the
|
|
|
|
cost of function calls.
|
2022-11-28 12:58:36 +00:00
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
(defn title-column []
|
|
|
|
{:height 56})
|
|
|
|
|
|
|
|
;; good
|
|
|
|
(def title-column
|
|
|
|
{:height 56})
|
|
|
|
```
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
(def community-card
|
|
|
|
{:background-color (colors/theme-colors colors/white colors/neutral-90)})
|
|
|
|
|
|
|
|
;; good
|
|
|
|
(defn community-card []
|
|
|
|
{:background-color (colors/theme-colors colors/white colors/neutral-90)})
|
|
|
|
```
|
|
|
|
|
2023-03-11 16:29:11 +00:00
|
|
|
### Custom Colors
|
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
The Status designs have a lot of customization of user and group colors. For
|
|
|
|
consistency it is best to use `customization-color` as the prop key on pages and
|
|
|
|
components. This will help easily identify what pages and components in the
|
|
|
|
application are using customized colors.
|
2023-03-11 16:29:11 +00:00
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
2023-08-09 14:32:36 +00:00
|
|
|
(defn community-card [{keys [custom-color]}]
|
2023-03-11 16:29:11 +00:00
|
|
|
...)
|
|
|
|
|
|
|
|
;; good
|
2023-08-09 14:32:36 +00:00
|
|
|
(defn community-card [{keys [customization-color]}]
|
2023-03-11 16:29:11 +00:00
|
|
|
...)
|
|
|
|
```
|
|
|
|
|
2022-11-14 10:15:49 +00:00
|
|
|
### Using TODOs comments
|
|
|
|
|
|
|
|
_TODO_ comments are used extensively in the codebase, but prefer to use them
|
|
|
|
only when strictly necessary and when an issue is not enough to track the work
|
|
|
|
left to be done.
|
|
|
|
|
|
|
|
These are all good examples:
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; TODO(@username): <message>
|
|
|
|
;; TODO(@username): <message>, <issue URL>
|
|
|
|
;; TODO(YYYY-MM-DD): <message>
|
|
|
|
;; TODO(@username,YYYY-MM-DD): <message>
|
|
|
|
```
|
|
|
|
|
|
|
|
### Subscription names and event names
|
|
|
|
|
|
|
|
Always register events and subscriptions using a meaningful namespace, but don't
|
|
|
|
namespace them with `::`. We understand it's a controversial decision because
|
|
|
|
there are both pros and cons to such practice.
|
|
|
|
|
|
|
|
Whenever appropriate, it's also recommended to use _fake_ namespaces to convey
|
|
|
|
more knowledge in the keyword about which bounded context (domain) it refers to.
|
|
|
|
You may also use dots to convey hierarchical structures.
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
;; Don't use real namespaced keywords.
|
|
|
|
(re-frame/reg-sub
|
|
|
|
::profile-pictures-visibility
|
|
|
|
:<- [:multiaccount]
|
|
|
|
(fn [multiaccount]
|
|
|
|
(:profile-pictures-visibility multiaccount)))
|
|
|
|
|
|
|
|
;; good
|
|
|
|
;; Uses a fake namespaced keyword.
|
|
|
|
(re-frame/reg-sub
|
|
|
|
:profile/pictures-visibility
|
|
|
|
:<- [:multiaccount]
|
|
|
|
(fn [multiaccount]
|
|
|
|
(:profile-pictures-visibility multiaccount)))
|
|
|
|
|
|
|
|
;; better
|
|
|
|
;; Uses a fake namespaced keyword with a parent namespace (multiaccount).
|
|
|
|
(re-frame/reg-sub
|
|
|
|
:multiaccount.profile/pictures-visibility
|
|
|
|
:<- [:multiaccount]
|
|
|
|
(fn [multiaccount]
|
|
|
|
(:profile-pictures-visibility multiaccount)))
|
|
|
|
```
|
|
|
|
|
|
|
|
### Declaring view components
|
|
|
|
|
|
|
|
Use the simple `defn` to declare components. Don't use `utils.views/defview` and
|
|
|
|
`utils.views/letsubs`.
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
(utils.views/defview browser []
|
|
|
|
(utils.views/letsubs [window-width [:dimensions/window-width]]
|
|
|
|
(do-something window-width)))
|
|
|
|
|
|
|
|
;; good
|
|
|
|
(defn browser []
|
|
|
|
(let [window-width (rf/sub [:dimensions/window-width])]
|
|
|
|
(do-something window-width)))
|
|
|
|
```
|
|
|
|
|
2023-02-16 14:29:44 +00:00
|
|
|
### Use `[]` instead of `()` in Reagent components
|
2023-02-07 13:05:38 +00:00
|
|
|
|
|
|
|
- The `()` version [does NOT work with Form-2 and
|
|
|
|
Form-3](https://github.com/reagent-project/reagent/blob/master/doc/UsingSquareBracketsInsteadOfParens.md#a-further-significant-why)
|
|
|
|
components.
|
|
|
|
- Components defined with `[]` will be [more efficient at re-render
|
|
|
|
time](https://github.com/reagent-project/reagent/blob/master/doc/UsingSquareBracketsInsteadOfParens.md#which-and-why)
|
|
|
|
because they're interpreted by Reagent and transformed into distinct React
|
|
|
|
components, with their own lifecycle.
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
[rn/view
|
|
|
|
(message-card message)]
|
|
|
|
|
|
|
|
;; good
|
|
|
|
[rn/view
|
|
|
|
[message-card message]]
|
|
|
|
```
|
|
|
|
|
2022-11-14 10:15:49 +00:00
|
|
|
### Using re-frame subscriptions and dispatching events
|
|
|
|
|
|
|
|
Use the `utils.re-frame` namespace instead of `re-frame.core` to subscribe and
|
|
|
|
dispatch.
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
(ns my-namespace
|
|
|
|
(:require [re-frame.core :as rf]))
|
|
|
|
|
|
|
|
(let [username @(rf/subscribe [:username])]
|
|
|
|
[pressable/pressable {:on-press #(rf/dispatch [:do-something])}
|
|
|
|
[rn/view
|
|
|
|
(str "Hello " username)]])
|
|
|
|
|
|
|
|
;; good
|
|
|
|
(ns my-namespace
|
|
|
|
(:require [utils.re-frame :as rf]))
|
|
|
|
|
|
|
|
(let [username (rf/sub [:username])]
|
|
|
|
[pressable/pressable {:on-press #(rf/dispatch [:do-something])}
|
|
|
|
[rn/view
|
|
|
|
(str "Hello " username)]])
|
|
|
|
```
|
|
|
|
|
Make json-rpc/call effect more in line with re-frame standards (#15936)
Changes effect :json/rpc-call to accept on-success and on-error parameters
either as functions OR as re-frame event vectors. The changes are 100% backwards
compatible.
## Why?
Re-frame is very explicit in its documentation and its architecture, saying that
event handlers should be pure. Calling re-frame.core/dispatch in event handlers
makes them sort of stateful.
> So, you can "get away with it". But it ain't pure.
>
> -- https://day8.github.io/re-frame/EffectfulHandlers/#90-solution
In status-mobile, arguably one of our most important effects (not to be confused
with event handlers) is :json-rpc/call, but at the moment, the on-success and
on-error values are expected to be stateful functions (e.g. usually used for
logging and dispatching subsequent events).
This creates two important problems:
1. The value returned by event handlers is more opaque and cannot be easily
inspected (for example using tap>, log/debug or just println). If we try to
inspect or log them, on-success and on-error will be printed as
#object[Function].
2. Testing event handlers using :json-rpc/call becomes an exercise of
frustration, because we can't simply compare the results of the event handler
with a good expected value, which is one of the big selling points of testing
pure functions.
### The testability of event handlers
> For any re-frame app, there's three things to test:
>
> - Event Handlers - most of your testing focus will be here because this is
> where most of the logic lives
> - Subscription Handlers - often not a lot to test here. Only Layer 3
> subscriptions need testing.
> - View functions - I don't tend to write tests for views.
>
> -- https://day8.github.io/re-frame/Testing/#what-to-test
So re-frame is saying event handlers should be pure, and that event handlers
should be tested.
In order to achieve the divine simplicity of testing event handlers as pure
functions, we need to make :json-rpc/call also accept on-success and
on-error as event vectors.
Good news is that there is a known pattern to solve this problem, e.g. used by
the library https://github.com/Day8/re-frame-http-fx.
The pattern is simple once we see it: behind the scenes, :json-rpc/call conj'es
the results of the RPC call into the event vectors on-success and on-error, and
:json-rpc/call dispatches the events itself. This eliminates the need for the
stateful dispatch call in event handlers.
2023-05-18 18:56:10 +00:00
|
|
|
### Registering effects
|
|
|
|
|
|
|
|
When registering re-frame effects (`reg-fx`), prefer to expose a data-only
|
|
|
|
interface because that will allow event handlers to stay pure.
|
|
|
|
|
|
|
|
For instance, if an effect needs a `on-success` callback, allow it to receive a
|
|
|
|
*re-frame event vector*. This approach is used by us in the [json-rpc/call
|
|
|
|
effect](src/status_im2/common/json_rpc/events.cljs), but also by third-party
|
|
|
|
effects, such as https://github.com/Day8/re-frame-http-fx. For the complete
|
|
|
|
rationale, see [PR #15936](https://github.com/status-im/status-mobile/pull/15936).
|
|
|
|
|
|
|
|
### Using the effect `:json-rpc/call`
|
|
|
|
|
|
|
|
Prefer the pure version of `:json-rpc/call` (no callbacks).
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; not as good
|
|
|
|
(rf/defn accept-contact-request
|
|
|
|
{:events [:activity-center.contact-requests/accept]}
|
|
|
|
[_ contact-id]
|
|
|
|
{:json-rpc/call
|
|
|
|
[{:method "wakuext_acceptContactRequest"
|
|
|
|
:params [{:id contact-id}]
|
|
|
|
:on-success #(rf/dispatch [:sanitize-messages-and-process-response %])
|
|
|
|
:on-error #(rf/dispatch [:activity-center.contact-requests/accept-error contact-id %])}]})
|
|
|
|
|
|
|
|
;; better
|
|
|
|
(rf/defn accept-contact-request
|
|
|
|
{:events [:activity-center.contact-requests/accept]}
|
|
|
|
[_ contact-id]
|
|
|
|
{:json-rpc/call
|
|
|
|
[{:method "wakuext_acceptContactRequest"
|
|
|
|
:params [{:id contact-id}]
|
|
|
|
:on-success [:sanitize-messages-and-process-response]
|
|
|
|
:on-error [:activity-center.contact-requests/accept-error contact-id]}]})
|
|
|
|
```
|
|
|
|
|
2022-11-14 10:15:49 +00:00
|
|
|
### Registering event handlers
|
|
|
|
|
|
|
|
Events must always be declared with the `utils.fx/defn` macro. Also, don't use
|
|
|
|
`re-frame.core/reg-event-db`.
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
(re-frame/reg-event-fx
|
|
|
|
:wakuv2.ui/save-all-confirmed
|
|
|
|
(fn [{:keys [db] :as cofx}]
|
|
|
|
...))
|
|
|
|
|
|
|
|
;; good
|
|
|
|
(fx/defn save-all
|
|
|
|
{:events [:wakuv2.ui/save-all-confirmed]}
|
|
|
|
[{:keys [db] :as cofx}]
|
|
|
|
...)
|
|
|
|
```
|
|
|
|
|
|
|
|
### Registering top-level re-frame subscriptions
|
|
|
|
|
|
|
|
Use `subs.root/reg-root-key-sub` to register top-level (root) subscriptions.
|
|
|
|
Additionally, register root subscriptions in the `subs.root` namespace.
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
(re-frame/reg-sub
|
|
|
|
:view-id
|
|
|
|
(fn [db]
|
|
|
|
(:view-id db)))
|
|
|
|
|
|
|
|
;; good
|
|
|
|
(reg-root-key-sub :view-id :view-id)
|
|
|
|
```
|
|
|
|
|
|
|
|
### Registering layer-3 subscriptions
|
|
|
|
|
|
|
|
The majority of the subscriptions should be defined as [layer-3
|
|
|
|
subscriptions](https://day8.github.io/re-frame/subscriptions/#the-four-layer)
|
|
|
|
due to performance constraints.
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
(re-frame/reg-sub
|
|
|
|
:ens/preferred-name
|
|
|
|
(fn [db]
|
|
|
|
(get-in db [:multiaccount :preferred-name])))
|
|
|
|
|
|
|
|
;; good
|
|
|
|
(re-frame/reg-sub
|
|
|
|
:ens/preferred-name
|
|
|
|
:<- [:multiaccount]
|
|
|
|
(fn [multiaccount]
|
|
|
|
(:preferred-name multiaccount)))
|
|
|
|
```
|
|
|
|
|
|
|
|
### Requiring quo2 components
|
|
|
|
|
|
|
|
Consume `quo2` components from `quo2.core`, unless the namespace is also inside
|
|
|
|
the `quo2/` directory.
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
(ns my-namespace
|
|
|
|
(:require [quo2.components.icon :as icon]))
|
|
|
|
|
|
|
|
(icon/icon :i/verified)
|
|
|
|
|
|
|
|
;; good
|
|
|
|
(ns my-namespace
|
2023-08-09 14:32:36 +00:00
|
|
|
(:require [quo2.core :as quo]))
|
2022-11-14 10:15:49 +00:00
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
(quo/icon :i/verified)
|
2022-11-14 10:15:49 +00:00
|
|
|
|
|
|
|
;; also good because both namespaces are inside quo2/
|
|
|
|
(ns quo2.components.tabs.account-selector
|
|
|
|
(:require [quo2.components.markdown.text :as text]))
|
|
|
|
```
|
|
|
|
|
|
|
|
### Require/import
|
|
|
|
|
|
|
|
Prefer `:as` instead of `:refer`. There are exceptions to this rule, e.g. the
|
|
|
|
test macros `deftest` and `is`, which are ubiquitous in the Clojure community.
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
(ns status-im.utils.datetime
|
|
|
|
(:require [cljs-time.coerce :refer [from-long]]))
|
|
|
|
|
|
|
|
;; good
|
|
|
|
(ns status-im.utils.datetime
|
|
|
|
(:require [cljs-time.coerce :as time.coerce]))
|
|
|
|
```
|
|
|
|
|
2022-11-28 12:58:36 +00:00
|
|
|
### Javascript interop
|
|
|
|
|
|
|
|
Use [binaryage/oops](https://github.com/binaryage/cljs-oops) macros instead of
|
|
|
|
core interop macros.
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
(fn [^js event]
|
|
|
|
(.-width (.-nativeEvent event)))
|
|
|
|
|
|
|
|
;; good
|
|
|
|
(require '[oops.core :as oops])
|
|
|
|
(fn [event]
|
|
|
|
(oops/oget event "nativeEvent.width"))
|
|
|
|
```
|
|
|
|
|
2022-11-14 10:15:49 +00:00
|
|
|
### Accessibility labels
|
|
|
|
|
2023-02-07 13:05:38 +00:00
|
|
|
Accessibility labels are currently used only for end-to-end tests. Use keywords
|
|
|
|
instead of strings (remember keywords are cached).
|
2022-11-14 10:15:49 +00:00
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
[text/text {:accessibility-label "profile-nickname"}
|
|
|
|
"Markov"]
|
|
|
|
|
|
|
|
;; good
|
|
|
|
[text/text {:accessibility-label :profile-nickname}
|
|
|
|
"Markov"]
|
|
|
|
```
|
|
|
|
|
2023-02-07 13:05:38 +00:00
|
|
|
Avoid dynamic labels, for example to specify an element's index because
|
|
|
|
[Appium](https://appium.io/) already supports element selection based on
|
|
|
|
indices.
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
[button {:accessibility-label (str "do-something" index)}]
|
|
|
|
|
|
|
|
;; good
|
|
|
|
[button {:accessibility-label :do-something}]
|
|
|
|
```
|
|
|
|
|
2022-11-14 10:15:49 +00:00
|
|
|
### Icons
|
|
|
|
|
2023-08-09 14:32:36 +00:00
|
|
|
Use the appropriate keyword qualification/namespace.
|
2022-11-14 10:15:49 +00:00
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
(require '[quo2.components.icon :as icons])
|
|
|
|
(icons/icon :main-icons2/verified)
|
|
|
|
|
|
|
|
;; good
|
|
|
|
(require '[quo2.core :as quo2])
|
|
|
|
(quo2/icon :i/verified)
|
|
|
|
```
|
|
|
|
|
|
|
|
### Translations
|
|
|
|
|
|
|
|
Prefer to use translation placeholders instead of creating multiple translation
|
|
|
|
keywords and concatenating them into a single string.
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
;; Assume the translation key is:
|
|
|
|
;; "biometric-auth-error": "Unable perform biometric authentication"
|
|
|
|
(str (i18n/label :t/biometric-auth-error) "(" error-code ")")
|
|
|
|
|
|
|
|
;; good
|
|
|
|
;; Assume the translation key is:
|
|
|
|
;; "biometric-auth-error": "Unable perform biometric authentication ({{code}})"
|
|
|
|
(i18n/label :t/biometric-auth-error {:code error-code})
|
|
|
|
```
|
|
|
|
|
2022-12-06 16:36:05 +00:00
|
|
|
### Tests
|
|
|
|
#### Subscription tests
|
|
|
|
|
|
|
|
Test [layer-3 subscriptions](https://day8.github.io/re-frame/subscriptions/) by
|
|
|
|
actually subscribing to them, so reframe's signal graph gets validated too.
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
;; bad
|
|
|
|
(defn user-recipes
|
|
|
|
[[current-user all-recipes location]]
|
|
|
|
...)
|
|
|
|
|
|
|
|
(re-frame/reg-sub
|
|
|
|
:user/recipes
|
|
|
|
:<- [:current-user]
|
|
|
|
:<- [:all-recipes]
|
|
|
|
:<- [:location]
|
|
|
|
user-recipes)
|
|
|
|
|
|
|
|
(deftest user-recipes-test
|
|
|
|
(testing "builds list of recipes"
|
|
|
|
(let [current-user {...}
|
|
|
|
all-recipes {...}
|
|
|
|
location [...]]
|
|
|
|
(is (= expected (recipes [current-user all-recipes location]))))))
|
|
|
|
|
|
|
|
;; good
|
2023-01-05 14:58:37 +00:00
|
|
|
(require '[test-helpers.unit :as h])
|
2022-12-06 16:36:05 +00:00
|
|
|
|
|
|
|
(re-frame/reg-sub
|
|
|
|
:user/recipes
|
|
|
|
:<- [:current-user]
|
|
|
|
:<- [:all-recipes]
|
|
|
|
:<- [:location]
|
|
|
|
(fn [[current-user all-recipes location]]
|
|
|
|
...))
|
|
|
|
|
|
|
|
(h/deftest-sub :user/recipes
|
|
|
|
[sub-name]
|
|
|
|
(testing "builds list of recipes"
|
|
|
|
(swap! rf-db/app-db assoc
|
|
|
|
:current-user {...}
|
|
|
|
:all-recipes {...}
|
|
|
|
:location [...])
|
|
|
|
(is (= expected (rf/sub [sub-name])))))
|
|
|
|
```
|
|
|
|
|
2022-11-14 10:15:49 +00:00
|
|
|
## Project Structure
|
|
|
|
|
|
|
|
First, the bird's-eye view with some example ClojureScript files:
|
|
|
|
|
|
|
|
```
|
|
|
|
src
|
|
|
|
├── js/
|
|
|
|
├── mocks/
|
|
|
|
├── quo2
|
|
|
|
│ ├── components/
|
|
|
|
│ ├── foundations/
|
|
|
|
│ └── theme.cljs
|
|
|
|
├── react_native
|
|
|
|
│ ├── gesture.cljs
|
|
|
|
│ └── platform.cljs
|
2022-12-28 15:21:15 +00:00
|
|
|
├── status_im/
|
|
|
|
├── status_im2
|
2022-11-14 10:15:49 +00:00
|
|
|
│ ├── common
|
|
|
|
│ │ └── components
|
|
|
|
│ │ └── bottom_sheet.cljs
|
|
|
|
│ ├── contexts/
|
|
|
|
│ ├── setup/
|
|
|
|
│ └── subs/
|
2022-12-28 15:21:15 +00:00
|
|
|
├── test_helpers/
|
2022-11-14 10:15:49 +00:00
|
|
|
└── utils.cljs
|
|
|
|
```
|
|
|
|
|
|
|
|
- `src/js`: Raw Javascript files, e.g. React Native Reanimated worklets.
|
|
|
|
- `src/mocks`: Plumbing configuration to be able to run tests.
|
|
|
|
- `src/quo2/`: The component library for Status Mobile.
|
|
|
|
- `src/react_native/`: Contains only low-level constructs to help React Native
|
|
|
|
work in tandem with Clojure(Script).
|
2022-12-28 15:21:15 +00:00
|
|
|
- `src/status_im2/`: Directory where we try to be as strict as possible about
|
|
|
|
our guidelines and where we prefer to write code for the new, redesigned
|
|
|
|
mobile app.
|
|
|
|
- `src/status_im/`: Directory containing what we call "old code", not yet
|
|
|
|
migrated to new guidelines for the new mobile app.
|
|
|
|
- `src/status_im2/common/`: Directories named `common` can appear at any level
|
|
|
|
of the directory tree. Just like directories named `utils`, their directory
|
2022-11-14 10:15:49 +00:00
|
|
|
nesting level communicates their applicable limits.
|
2022-12-28 15:21:15 +00:00
|
|
|
- `src/status_im2/common/components/`: Contains reusable components that are not
|
2022-11-14 10:15:49 +00:00
|
|
|
part of the design system (quo2).
|
2022-12-28 15:21:15 +00:00
|
|
|
- `src/status_im2/contexts/`: Contains [bounded contexts](#glossary), like
|
2022-11-14 10:15:49 +00:00
|
|
|
`browser/`, `messaging/`, etc. As much as possible, _bounded contexts_ should
|
|
|
|
not directly require each other's namespaces.
|
2022-12-28 15:21:15 +00:00
|
|
|
- `src/status_im2/setup/`: Contains namespaces that are mostly used to
|
|
|
|
initialize the application, configure test runners, etc. In general, such
|
|
|
|
namespaces should not be required from the outside.
|
|
|
|
- `src/test_helpers/`: Reusable utilities for writing all kinds of tests.
|
2022-11-14 10:15:49 +00:00
|
|
|
- `src/status_im/subs/`: All subscriptions should live inside it.
|
|
|
|
|
|
|
|
Directories named `utils/` can appear at any level of the directory tree. The
|
|
|
|
directory nesting level precisely indicates its boundaries. For example, a
|
|
|
|
`contexts/user_settings/utils/datetime.cljs` file communicates that it should
|
|
|
|
only be used in the `user_settings` context.
|
|
|
|
|
|
|
|
### src/quo2
|
|
|
|
|
|
|
|
The `src/quo2/` directory holds all components for the new design system. As
|
|
|
|
much as possible, its sub-directories and component names should reflect the
|
|
|
|
same language used by designers.
|
|
|
|
|
|
|
|
Even though the directory lives alongside the rest of the codebase, we should
|
|
|
|
think of it as an external entity that abstracts away particular Status domain
|
|
|
|
knowledge.
|
|
|
|
|
|
|
|
Components inside `src/quo2/` should not rely on re-frame, i.e. they should not
|
|
|
|
dispatch events or use subscriptions.
|
|
|
|
|
|
|
|
Example structure:
|
|
|
|
|
|
|
|
```
|
|
|
|
src
|
|
|
|
└── quo2
|
|
|
|
├── components
|
|
|
|
│ └── dropdown
|
|
|
|
│ ├── style.cljs
|
|
|
|
│ ├── test.cljs
|
|
|
|
│ └── view.cljs
|
|
|
|
└── screens
|
|
|
|
└── dropdown
|
|
|
|
└── view.cljs
|
|
|
|
```
|
|
|
|
|
|
|
|
### Re-frame events
|
|
|
|
|
|
|
|
Event handlers should be defined in files named `events.cljs`, and they should
|
|
|
|
be _close_ to other _things_, like view files, components, etc.
|
|
|
|
|
|
|
|
For example:
|
|
|
|
|
|
|
|
```
|
|
|
|
src
|
|
|
|
└── contexts
|
|
|
|
└── browser
|
|
|
|
├── bookmarks/
|
|
|
|
├── options/
|
|
|
|
├── permissions/
|
|
|
|
├── events.cljs
|
|
|
|
├── events_test.cljs
|
|
|
|
├── style.cljs
|
|
|
|
└── view.cljs
|
|
|
|
```
|
|
|
|
|
|
|
|
### Test structure
|
|
|
|
|
|
|
|
[Unit tests](#glossary) should be created alongside their respective source
|
|
|
|
implementation. We prefer them colocated with the source and not like most
|
|
|
|
Clojure (JVM) codebases which mirror the sources in a top-level test directory.
|
|
|
|
|
|
|
|
```
|
|
|
|
├── models
|
|
|
|
│ ├── message.cljs
|
|
|
|
│ └── message_test.cljs
|
|
|
|
├── models.cljs
|
|
|
|
└── models_test.cljs
|
|
|
|
```
|
|
|
|
|
Upgrade shadow-cljs and ClojureScript (#15417)
This commit upgrades Shadow CLJS from 2.11.16 (released on Feb/21) to latest
2.25.0 (Jul/23), so ~1.5 years worth of upgrades. By upgrading shadow we
can finally use the latest major Clojure version 1.11.x.
Why upgrade shadow?
- Shadow CLJS controls the ClojureScript version we can use. In order to use the
latest major Clojure version we must upgrade Shadow CLJS.
- Shadow CLJS releases new versions very frequently, and if you take a look at
its changelog https://github.com/thheller/shadow-cljs/blob/master/CHANGELOG.md, you'll see
it had tons and tons of bug fixes over the years. I hope some of them help
improve the DX for certain contributors who recently reported issues with
it.
- Clojure 1.11 brings new features, bug fixes and even performance improvements
(although I think the performance mostly impacts Clojure on the JVM). See the
changelog https://github.com/clojure/clojure/blob/master/changes.md#changes-to-clojure-in-version-1110
Things that can be beneficial to us, or are interesting nonetheless:
- New :as-alias to be used in require, which is like :as but does not require
the namespace to load. This means namespaced keywords using :as-alias can't
cause circular dependency errors. This feature would very useful if we used
namespaced keywords, but we don't, so...
https://github.com/clojure/clojure/blob/master/changes.md#22-as-alias-in-require
- New macros run-test and run-test-var to run single test with fixtures and
report.
- New iteration function, useful for processing paginated data.
https://www.abhinavomprakash.com/posts/clojure-iteration/
- New update-keys function: applies a function to every key in a map.
- New update-vals function: applies a function to every value in a map.
Examples for update-vals and update-keys. They should perform better than the
common reduce-kv approach since they use a transient data structure.
(let [m {:a 1 :b 2}]
(update-vals m inc)) ; => {:a 2, :b 3}
(let [m {:a 1 :b 2}]
(update-keys m name)) ; => {"a" 1, "b" 2}
Why change namespaces within __tests__ directories?
Any namespace with the word --tests-- throws an error, like the one below. I
didn't bother investigating why, so I changed the guidelines to reflect the new
convention. It's probably related to the double dashes in the name.
Namespace quo2.components.dividers.--tests--.divider-label-component-spec has a
segment starting with an invalid JavaScript identifier at line 1
2023-07-28 16:40:54 +00:00
|
|
|
Component tests should be created in the same directory as the source component,
|
|
|
|
and named as `component_spec.cljs`.
|
|
|
|
|
2022-12-28 15:21:15 +00:00
|
|
|
|
|
|
|
```
|
|
|
|
└── filter
|
|
|
|
├── component_spec.cljs
|
|
|
|
├── style.cljs
|
|
|
|
└── view.cljs
|
|
|
|
```
|
|
|
|
|
2022-11-14 10:15:49 +00:00
|
|
|
There's no hard rule on how integration test namespaces should be split, but
|
|
|
|
we're at least striving to define them under appropriate bounded contexts that
|
|
|
|
mirror the source code.
|
|
|
|
|
|
|
|
```
|
|
|
|
test
|
|
|
|
├── appium/
|
|
|
|
└── integration
|
|
|
|
├── browser/
|
|
|
|
├── communities/
|
|
|
|
├── messaging/
|
|
|
|
├── user_settings/
|
|
|
|
└── wallet
|
|
|
|
└── payment_test.cljs
|
|
|
|
```
|
|
|
|
|
|
|
|
## Glossary
|
|
|
|
|
|
|
|
**Unit test**: The smallest atomic unit that's meaningful to test. For example,
|
|
|
|
tests for utility functions and event handlers are considered unit tests in the
|
|
|
|
mobile codebase. They should be completely deterministic, _fast_, and they
|
|
|
|
should work flawlessly in the REPL.
|
|
|
|
|
|
|
|
**Bounded context**: A logical separation between different domains. It's an
|
|
|
|
important concept in the [Domain-Driven
|
|
|
|
Design](https://en.wikipedia.org/wiki/Domain-driven_design) literature. See
|
|
|
|
[Bounded Context, by Martin
|
|
|
|
Fowler](https://martinfowler.com/bliki/BoundedContext.html) for an introduction
|
|
|
|
to the topic.
|