├── .editorconfig
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── CONTRIBUTORS.edn
├── LICENSE.md
├── README.md
├── deps.edn
├── dev-src
└── braid
│ └── dev
│ ├── core.clj
│ └── figwheel.clj
├── dev.cljs.edn
├── docs
├── dev
│ ├── code-organization.md
│ ├── developing-bots.md
│ └── getting-up-and-running-in-development.md
├── drafts
│ ├── architecture.md
│ ├── background
│ │ ├── chatrooms-considered-harmful.md
│ │ ├── faq.md
│ │ ├── infrastructure-should-be-open.md
│ │ ├── similar.md
│ │ └── towards-productive-communication.md
│ ├── contributing-other.md
│ ├── dev-getting-started.md
│ ├── developing-modules.md
│ ├── guidelines-code.md
│ ├── guidelines-design.md
│ ├── guidelines-documentation.md
│ └── stack.md
├── images
│ ├── braid-icon-256.png
│ ├── screenshot.png
│ └── youtube-player.png
├── on-prem
│ └── installing-on-prem.md
└── responsible-disclosure-policy.md
├── gateway.cljs.edn
├── prod.cljs.edn
├── profiles.sample.clj
├── project.clj
├── resources
├── CHANGELOG.md
├── public
│ ├── desktop.html
│ ├── fonts
│ │ ├── fa-brands-400.eot
│ │ ├── fa-brands-400.svg
│ │ ├── fa-brands-400.ttf
│ │ ├── fa-brands-400.woff
│ │ ├── fa-brands-400.woff2
│ │ ├── fa-solid-900.eot
│ │ ├── fa-solid-900.svg
│ │ ├── fa-solid-900.ttf
│ │ ├── fa-solid-900.woff
│ │ └── fa-solid-900.woff2
│ ├── gateway.html
│ ├── images
│ │ ├── braid-logo-color.svg
│ │ ├── braid.svg
│ │ ├── favicon.png
│ │ └── quests
│ │ │ ├── conversation-close.gif
│ │ │ ├── conversation-new.gif
│ │ │ └── conversation-reply.gif
│ └── oauth-post-auth.html
└── templates
│ ├── email_digest.html.mustache
│ ├── email_digest.txt.mustache
│ ├── link_signup.html.mustache
│ ├── register_page.html.mustache
│ └── reset_page.html.mustache
├── scripts
└── video-convert.sh
├── src
└── braid
│ ├── base
│ ├── api.cljc
│ ├── client
│ │ ├── events.cljs
│ │ ├── pages.cljs
│ │ ├── remote_handlers.cljs
│ │ ├── root_view.cljs
│ │ ├── router.cljs
│ │ ├── socket.cljs
│ │ ├── state.cljs
│ │ ├── styles.cljs
│ │ └── subs.cljs
│ ├── conf.clj
│ ├── conf_extra.clj
│ ├── core.cljc
│ └── server
│ │ ├── cache.clj
│ │ ├── cqrs.clj
│ │ ├── cqrs_fx.clj
│ │ ├── http_api_routes.clj
│ │ ├── http_client_routes.clj
│ │ ├── initial_data.clj
│ │ ├── jobs.clj
│ │ ├── scheduler.clj
│ │ ├── schema.clj
│ │ ├── seed.clj
│ │ ├── socket.clj
│ │ ├── spa.clj
│ │ └── ws_handler.clj
│ ├── bots
│ ├── client
│ │ ├── autocomplete.cljs
│ │ ├── events.cljs
│ │ ├── remote_handlers.cljs
│ │ ├── subs.cljs
│ │ └── views
│ │ │ ├── bot_sender_view.cljs
│ │ │ ├── bots_page.cljs
│ │ │ └── bots_page_styles.cljs
│ ├── core.cljc
│ ├── schema.cljc
│ ├── server.clj
│ ├── server
│ │ ├── db.clj
│ │ ├── routes.clj
│ │ └── sync.clj
│ └── util.cljc
│ ├── chat
│ ├── api.cljc
│ ├── client
│ │ ├── events.cljs
│ │ ├── remote_handlers.cljs
│ │ └── subs.cljs
│ ├── commands.clj
│ ├── core.cljc
│ ├── db
│ │ ├── common.clj
│ │ ├── group.clj
│ │ ├── invitation.clj
│ │ ├── message.clj
│ │ ├── tag.clj
│ │ ├── thread.clj
│ │ └── user.clj
│ ├── events.clj
│ ├── predicates.clj
│ ├── schema.clj
│ ├── seed.clj
│ ├── server
│ │ └── initial_data.clj
│ └── socket_message_handlers.clj
│ ├── core.clj
│ ├── core
│ ├── client
│ │ ├── desktop
│ │ │ ├── core.cljs
│ │ │ └── notify.cljs
│ │ ├── gateway
│ │ │ ├── core.cljs
│ │ │ ├── events.cljs
│ │ │ ├── forms
│ │ │ │ ├── join_group
│ │ │ │ │ └── events.cljs
│ │ │ │ └── user_auth
│ │ │ │ │ ├── events.cljs
│ │ │ │ │ ├── styles.cljs
│ │ │ │ │ ├── subs.cljs
│ │ │ │ │ ├── validations.cljs
│ │ │ │ │ └── views.cljs
│ │ │ ├── fx.cljs
│ │ │ ├── helper_views.cljs
│ │ │ ├── helpers.cljs
│ │ │ ├── styles.cljs
│ │ │ ├── styles_vars.cljs
│ │ │ ├── subs.cljs
│ │ │ └── views.cljs
│ │ ├── group_admin
│ │ │ ├── events.cljs
│ │ │ ├── subs.cljs
│ │ │ └── views
│ │ │ │ ├── group_settings_page.cljs
│ │ │ │ └── group_settings_page_styles.cljs
│ │ ├── helpers.cljs
│ │ ├── invites
│ │ │ ├── events.cljs
│ │ │ ├── schema.cljs
│ │ │ ├── subs.cljs
│ │ │ └── views
│ │ │ │ ├── invite.cljs
│ │ │ │ ├── invite_page.cljs
│ │ │ │ └── invite_page_styles.cljs
│ │ ├── routes.cljs
│ │ ├── schema.cljs
│ │ ├── state
│ │ │ ├── fx
│ │ │ │ ├── dispatch_debounce.cljs
│ │ │ │ └── redirect.cljs
│ │ │ ├── helpers.cljs
│ │ │ └── subscription.cljs
│ │ ├── store.cljs
│ │ └── ui
│ │ │ ├── styles
│ │ │ ├── animations.cljs
│ │ │ ├── body.cljs
│ │ │ ├── fontawesome.cljs
│ │ │ ├── header.cljs
│ │ │ ├── hljs.cljs
│ │ │ ├── hover_cards.cljs
│ │ │ ├── hover_menu.cljs
│ │ │ ├── imports.cljs
│ │ │ ├── message.cljs
│ │ │ ├── misc.cljs
│ │ │ ├── mixins.cljs
│ │ │ ├── page.cljs
│ │ │ ├── pages
│ │ │ │ ├── global_settings.cljs
│ │ │ │ └── me.cljs
│ │ │ ├── pills.cljs
│ │ │ ├── thread.cljs
│ │ │ ├── threads.cljs
│ │ │ └── vars.cljs
│ │ │ └── views
│ │ │ ├── app.cljs
│ │ │ ├── autocomplete.cljs
│ │ │ ├── card_border.cljs
│ │ │ ├── header.cljs
│ │ │ ├── header_item.cljs
│ │ │ ├── hover_menu.cljs
│ │ │ ├── main.cljs
│ │ │ ├── mentions.cljs
│ │ │ ├── message.cljs
│ │ │ ├── new_message.cljs
│ │ │ ├── new_message_action_button.cljs
│ │ │ ├── pages
│ │ │ ├── changelog.cljs
│ │ │ ├── global_settings.cljs
│ │ │ ├── me.cljs
│ │ │ └── readonly.cljs
│ │ │ ├── pills.cljs
│ │ │ ├── styles.cljs
│ │ │ ├── subscribe_button.cljs
│ │ │ ├── tag_hover_card.cljs
│ │ │ ├── thread.cljs
│ │ │ ├── thread.cljs.rej
│ │ │ ├── thread_header.cljs
│ │ │ ├── threads.cljs
│ │ │ ├── upload.cljs
│ │ │ ├── user_header.cljs
│ │ │ └── user_hover_card.cljs
│ ├── common
│ │ ├── schema.cljc
│ │ └── util.cljc
│ ├── hooks.cljc
│ ├── modules.cljc
│ └── server
│ │ ├── core.clj
│ │ ├── db.clj
│ │ ├── email_digest.clj
│ │ ├── handler.clj
│ │ ├── invite.clj
│ │ ├── mail.clj
│ │ ├── message_format.clj
│ │ ├── middleware.clj
│ │ ├── migrate.clj
│ │ ├── notify_rules.clj
│ │ ├── routes
│ │ ├── api
│ │ │ ├── private.clj
│ │ │ └── public.clj
│ │ ├── client.clj
│ │ ├── helpers.clj
│ │ └── socket.clj
│ │ ├── sync_handler.clj
│ │ └── sync_helpers.clj
│ ├── disconnect_notice
│ ├── core.cljc
│ ├── styles.cljs
│ └── ui.cljs
│ ├── embeds
│ ├── api.cljc
│ ├── core.cljc
│ ├── impl.cljs
│ └── styles.cljs
│ ├── embeds_image
│ └── core.cljc
│ ├── embeds_map
│ └── core.cljc
│ ├── embeds_video
│ └── core.cljc
│ ├── embeds_website
│ ├── core.cljc
│ ├── link_extract.clj
│ ├── styles.cljs
│ └── views.cljs
│ ├── embeds_youtube
│ └── core.cljc
│ ├── emoji
│ ├── api.cljc
│ ├── client
│ │ ├── autocomplete.cljs
│ │ ├── lookup.cljs
│ │ ├── styles.cljs
│ │ ├── text_replacements.cljs
│ │ └── views.cljs
│ └── core.cljc
│ ├── emoji_big
│ └── core.cljc
│ ├── emoji_custom
│ ├── client
│ │ ├── autocomplete.cljs
│ │ ├── state.cljs
│ │ ├── styles.cljs
│ │ └── views.cljs
│ ├── core.cljc
│ └── server
│ │ ├── core.clj
│ │ └── db.clj
│ ├── emoji_emojione
│ ├── core.cljc
│ ├── impl.cljs
│ └── ref.cljs
│ ├── group_create
│ ├── core.cljc
│ ├── styles.cljs
│ ├── test.clj
│ ├── validations.cljs
│ └── views.cljs
│ ├── group_explore
│ ├── api.cljc
│ ├── core.cljc
│ ├── styles.cljs
│ └── views.cljs
│ ├── lib
│ ├── aws.clj
│ ├── color.cljs
│ ├── crypto.clj
│ ├── date.cljs
│ ├── digest.clj
│ ├── github.clj
│ ├── gravatar.clj
│ ├── markdown.clj
│ ├── misc.cljc
│ ├── noembed.clj
│ ├── s3.clj
│ ├── s3.cljs
│ ├── transit.clj
│ ├── upload.cljs
│ ├── url.clj
│ ├── url.cljs
│ ├── uuid.cljc
│ └── xhr.cljs
│ ├── notices
│ └── core.cljc
│ ├── page_inbox
│ ├── commands.clj
│ ├── core.cljc
│ ├── styles.cljs
│ └── ui.cljs
│ ├── page_recent
│ └── core.cljc
│ ├── page_subscriptions
│ ├── core.cljc
│ ├── styles.cljs
│ └── ui.cljs
│ ├── page_uploads
│ ├── core.cljc
│ └── views
│ │ ├── uploads_page.cljs
│ │ └── uploads_page_styles.cljs
│ ├── permalinks
│ └── core.cljc
│ ├── popovers
│ ├── api.cljs
│ ├── core.cljc
│ ├── helpers.cljs
│ └── impl.cljs
│ ├── quests
│ ├── client
│ │ ├── core.cljs
│ │ ├── events.cljs
│ │ ├── helpers.cljs
│ │ ├── list.cljs
│ │ ├── remote_handlers.cljs
│ │ ├── styles.cljs
│ │ └── views.cljs
│ ├── core.cljc
│ └── server
│ │ ├── core.clj
│ │ └── db.clj
│ ├── rss
│ ├── client
│ │ └── views.cljs
│ ├── core.cljc
│ └── server
│ │ ├── db.clj
│ │ └── fetching.clj
│ ├── search
│ ├── api.cljc
│ ├── client.cljs
│ ├── core.cljc
│ ├── lucene.clj
│ ├── server.clj
│ ├── tags.clj
│ ├── threads.clj
│ ├── ui
│ │ ├── search_bar.cljs
│ │ ├── search_button.cljs
│ │ ├── search_page.cljs
│ │ ├── search_page_styles.cljs
│ │ ├── tag_results.cljs
│ │ ├── thread_results.cljs
│ │ └── user_results.cljs
│ └── users.clj
│ ├── sidebar
│ ├── api.cljs
│ ├── core.cljc
│ ├── styles.cljs
│ └── ui.cljs
│ ├── stars
│ └── core.cljc
│ ├── uploads
│ ├── core.cljc
│ └── db.clj
│ ├── users
│ ├── client
│ │ └── views
│ │ │ ├── users_page.cljs
│ │ │ └── users_page_styles.cljs
│ └── core.cljc
│ └── version
│ └── core.cljc
└── test
├── braid
└── test
│ ├── client
│ ├── runners
│ │ ├── doo.cljs
│ │ └── tests.cljs
│ └── ui
│ │ └── views
│ │ └── message_test.cljs
│ ├── fixtures
│ ├── conf.clj
│ └── db.clj
│ ├── lib
│ └── markdown_test.clj
│ └── server
│ ├── db
│ ├── group_test.clj
│ └── user_test.clj
│ ├── db_test.clj
│ ├── names_test.clj
│ ├── new_message_test.clj
│ ├── notify_rules_test.clj
│ ├── search_test.clj
│ ├── tags_test.clj
│ └── test_utils.clj
├── helpers
└── cookies.clj
├── integration
└── braid
│ └── system_start_stop_test.clj
└── unit
└── braid
├── core
└── server
│ └── middleware_test.clj
└── lib
└── s3_test.clj
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org/
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | indent_style = space
8 | end_of_line = lf
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /resources/public/js/
2 | /cljs-test-runner-out
3 | /target
4 | .lein-env
5 | .lein-repl-history
6 | .nrepl-port
7 | .rebel_readline_history
8 | *-init.clj
9 | profiles.clj
10 | .lein-failures
11 | .DS_Store
12 | Session.vim
13 | figwheel_server.log
14 | /.idea
15 | *.iml
16 | .cljs_rhino_repl/
17 | /notes/
18 | /datomic-pro-0.9.5201
19 | /datomic-pro-0.9.5697
20 | /lucene
21 | .cpcache
22 | /.calva
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Welcome! We're super excited that you are interested in contributing to Braid.
4 |
5 | There are a lot of ways you can be involved - not just coding - and all are valuable.
6 |
7 |
8 | ## Say Hello
9 |
10 | If you'd like to contribute, jump into the [Braid group on Braid](https://braid.chat/braid) and say hello. We can help match you with a good task, point you to resources and help you if you run into any problems.
11 |
12 | @jamesnvc and @rafd have a standing offer to remotely pair with anyone on Braid. Send a message on Braid chat if you'd like to take us up on it.
13 |
14 | We want to make contributing to Braid as painless as possible (and, ideally, gratifying). Please let us know how we can improve.
15 |
16 |
17 | ## Giving Feedback
18 |
19 | We want Braid to be useful to you, so we highly value your feedback.
20 |
21 | Right now, we're especially looking for feedback on:
22 |
23 | - the first-time user experience
24 | - the first-time contributor experience
25 | - the first-time developer experience
26 |
27 | You can give us feedback [Braid](https://braid.chat/braid) or via [Github Issues](https://github.com/braidchat/braid/issues).
28 |
29 |
30 | ### Reporting Issues
31 |
32 | Found a bug or some other issue? Let us know on [Braid](https://braid.chat/braid) or create a [Github Issue](https://github.com/braidchat/braid/issues).
33 |
34 | If it's a security issue, please do not post it publicly, and instead, email us at security@braidchat.com (see our [Responsible Disclosure Policy](./responsible-disclosure-policy.md).
35 |
36 | When reporting an issue, the following information helps us track it down and fix it faster:
37 |
38 | - steps to reproduce the issue
39 | - expected vs observed behaviour
40 | - relevant error messages and/or screenshots
41 | - possible fixes
42 |
43 |
44 | ### Contributing Feature Ideas
45 |
46 | If you have an idea for a new feature or change to Braid, we'd love to hear it. Let us know on [Braid](https://braid.chat/braid) or create a [Github Issue](https://github.com/braidchat/braid/issues).
47 |
48 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.edn:
--------------------------------------------------------------------------------
1 | ; Braid Contributors
2 |
3 | ; Thank you to all our contributors!
4 |
5 | ; This is an edn list that loosely follows:
6 | ; https://github.com/kentcdodds/all-contributors
7 |
8 | ; Also worth checking: Github's Network Members:
9 | ; https://github.com/braidchat/braid/network/members
10 |
11 | [
12 |
13 | {:name "Rafal D."
14 | :github "@rafd"
15 | :contributions ["project inception"]
16 | :tags #{:code
17 | :infrastructure
18 | :documentation
19 | :design
20 | :tests
21 | :answering-questions
22 | :bug-reports
23 | :pr-review
24 | :project-management}}
25 |
26 | {:name "James C."
27 | :github "@jamesnvc"
28 | :tags #{:code
29 | :plugins
30 | :infrastructure
31 | :tests
32 | :documentation
33 | :answering-questions
34 | :bug-reports
35 | :pr-review}}
36 |
37 | {:name "Brandon G."
38 | :github "@10plusY"
39 | :contributions ["port from Om to Reagent"
40 | "webrtc integration"]
41 | :tags #{:code
42 | :plugins}}
43 |
44 | {:name "Sam J."
45 | :github "@sjuraschka"
46 | :contributions ["client UX and UI improvements"]
47 | :tags #{:code
48 | :design}}
49 |
50 | {:github "@CommonCreative"
51 | :tags #{:code}}
52 |
53 | {:name "Dominic M."
54 | :github "@SevereOverfl0w"
55 | :tags #{:documentation}}
56 |
57 | {:github "@sveri"
58 | :contributions [""]
59 | :tags #{:documentation}}
60 |
61 | {:name "Canna W."
62 | :github "@cannawen"
63 | :tags #{:documentation}}
64 |
65 | ]
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/dev-src/braid/dev/figwheel.clj:
--------------------------------------------------------------------------------
1 | (ns braid.dev.figwheel
2 | (:require
3 | [figwheel.main.api :as repl-api]
4 | [mount.core :refer [defstate]]))
5 |
6 | (defstate figwheel
7 | :start (repl-api/start
8 | {:mode :serve
9 | :open-url false
10 | :ring-server-options {:port 3559}
11 | :connect-url "ws://[[client-hostname]]:[[server-port]]/figwheel-connect"
12 | :watch-dirs ["src"]}
13 | "dev")
14 | :stop (repl-api/stop-all))
15 |
--------------------------------------------------------------------------------
/dev.cljs.edn:
--------------------------------------------------------------------------------
1 | {:main braid.core.client.desktop.core
2 | :asset-path "/js/dev/desktop/"
3 | :output-to "resources/public/js/dev/desktop.js"
4 | :output-dir "resources/public/js/dev/desktop/"
5 | :optimizations :none
6 | :verbose false
7 | :closure-defines {"goog.DEBUG" true}
8 | :parallel-build true}
9 |
--------------------------------------------------------------------------------
/docs/drafts/architecture.md:
--------------------------------------------------------------------------------
1 | # Architecture
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/drafts/background/faq.md:
--------------------------------------------------------------------------------
1 | # FAQ
2 |
3 | A list of question or concerns raised by folks and responses.
4 |
5 | ## Why yet-another-chat-app?
6 |
7 | See [Motivation](https://github.com/braidchat/meta/wiki/Motivation)
8 |
9 | First prototype was a one-day hack but we loved it; shared it with some folks, they loved it; so here we are.
10 |
11 | ...different folks have different reasons for supporting Braid, ex. couldn't find an existing app that:
12 |
13 | - was hackable / open-source
14 | - promoted better conversations
15 | - was written in clojure(script)
16 | - ...
17 |
18 | (or a combination of some or all of the above)
19 |
20 | ## Why not build on top of xmpp / irc / matrix / etc.?
21 |
22 | Maybe we will.
23 |
24 | For now, we're focusing on improving the front-end experience, so the backend we already wrote is good enough.
25 |
26 | ## Why clojure + clojurescript? Why not...
27 |
28 | Clojure(script) is a fantastic language that lets us be more productive. [Read More](../dev/stack.md)
29 |
--------------------------------------------------------------------------------
/docs/drafts/background/infrastructure-should-be-open.md:
--------------------------------------------------------------------------------
1 | # Communication Infrastructure Should be Open
2 |
3 | (in progress)
4 |
5 | For many years, group chat on the internet was dominated by IRC, mailing lists, forums and XMPP.
6 |
7 | Presently, Slack is the king of group chat, eclipsing the previous technologies.
8 |
9 |
10 | ## Benefits of Open Source
11 |
12 | - not under risk of patron removing your rights
13 |
14 | (ex. Reactiflux community hitting Slack user limit and moving to Discord)
15 |
16 | - freedom to extend: anyone can add features
17 |
18 | - self-hosting, security, privacy
19 |
20 | - standards, interoperability
21 |
--------------------------------------------------------------------------------
/docs/drafts/background/similar.md:
--------------------------------------------------------------------------------
1 | # Similar Projects
2 |
3 | Scrollback
4 | https://scrollback.io/scrollback
5 |
6 | Flowdock
7 | https://www.flowdock.com/
8 |
9 | Euphoria / Heim
10 | https://euphoria.io/
11 | https://github.com/euphoria-io/heim
12 |
13 | Federated Wiki
14 | http://fed.wiki.org
15 |
16 | Discourse
17 | http://discourse.org
18 |
19 | Loomio
20 | https://www.loomio.org/
21 |
22 | Apache Wave (nee Google Wave)
23 | https://incubator.apache.org/wave/
24 |
25 | Zulip
26 | https://zulip.org/
27 |
28 | ## Discussions
29 |
30 | - https://news.ycombinator.com/item?id=9770322
31 | - https://news.ycombinator.com/item?id=10386847
32 | - https://news.ycombinator.com/item?id=10068943
33 |
34 | ## Direct IRC Alternatives
35 |
36 | - https://blog.okturtles.com/2015/11/five-open-source-slack-alternatives/
37 |
38 | - http://www.mattermost.org/
39 |
40 |
41 |
42 | ## Discussions
43 |
44 | - https://github.com/reactiflux/volunteers/issues/25
45 | - https://github.com/reactiflux/volunteers/issues/17
46 | - http://www.jordanhawker.com/posts/131477030371
47 | - https://news.ycombinator.com/item?id=9770322
48 | - https://news.ycombinator.com/item?id=10386847
49 | - https://news.ycombinator.com/item?id=10068943
50 |
--------------------------------------------------------------------------------
/docs/drafts/background/towards-productive-communication.md:
--------------------------------------------------------------------------------
1 | # Towards Productive Communication
2 |
3 | - target audience: teams and online communities
4 | - allow for both real-time and async experiences
5 | (ie. don't demand the group's full attention just to keep the conversation on track)
6 |
7 | Forums address some of these issues (but even threads still often have the tangent problem)... need to make threading easier, but this needs balance too (full threading results in fractal conversation, can also be difficult to follow)
8 |
9 | Braid's major changes are:
10 | - thinking with tags instead of rooms (similar to how Gmail thinks with tags vs folders);
11 | - per-topic conversations, instead of per-room chatter (similar to how you have Facebook comments on each post)
12 |
13 | [[images/braid-concept.png]]
14 |
15 | As a result...
16 |
17 | - you can have multiple simultaneous conversations related to a tag (vs. trying to keep track of multiple conversations in one room)
18 |
19 | - users can "mute" conversations (vs. having to deal with notifications for irrelevant messages in a room you want to stay in)
20 |
21 | - conversations can be resumed without context loss (vs. having to search for old messages)
22 |
23 | - no active conversations = your screen is empty (vs. old messages showing)
24 |
25 | - conversations can have multiple tags (vs. settling on the 'best' room)
26 |
27 | - conversations can change tags over time (vs. maintaining a conversation in an inappropriate room)
28 |
29 |
30 | Martin has some great thoughts here: http://www.martinklepsch.org/chaf.html
31 |
32 | More thoughts on threading here: https://hackpad.com/Chatroom-vs-....-bringing-threading-to-chat-8jJD3sxtRAf
33 |
--------------------------------------------------------------------------------
/docs/drafts/contributing-other.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | ### Feedback and Voting on Planned Features
10 |
11 | There are a lot of ideas in the [Braid Repo](https://github.com/braidchat/braid/issues), and your feedback can help us prioritize which ones to address. Give the existing ideas a look, and feel free to leave a comment or a :+1:.
12 |
13 |
14 | ### Triaging and Organizing Issues
15 |
16 | The [Braid Repo](https://github.com/braidchat/braid/issues) needs help staying organized (tagging issues, removing duplicates, removing stales issues, etc.) and if that sounds interesting to you, let us know.
17 |
18 |
19 | ## Writing
20 |
21 | ### Improving Documentation for Developers
22 |
23 | ### Improving Guides for Users
24 |
25 | ### Translating Braid to Other Languages
26 |
27 |
28 | ## Sharing
29 |
30 |
31 | ## Sponsoring
32 |
33 | ### Paid Hosting
34 |
35 | ### Gratipay
36 |
37 | ### Bountysource
38 |
39 |
40 | ## Coding
41 |
42 | ### Adressing "Help Wanted" Issues
43 |
44 | We've been tagging issues that are ideal for first-time committers with the [`help wanted`](https://github.com/braidchat/braid/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3A%22help%20wanted%22) tag on Braid Meta. Most of these issues can be addressed with minor code changes that don't require knowledge of the full system.
45 |
46 |
47 | ### Contributing to Braid Core
48 |
49 |
50 | ### Writing End-to-End Tests
51 |
52 |
53 | ### Developing Integrations
54 |
55 |
--------------------------------------------------------------------------------
/docs/drafts/dev-getting-started.md:
--------------------------------------------------------------------------------
1 | # Contributing: Coding
2 |
3 |
4 |
5 | ## Editor
6 |
7 | If you're just starting out with Clojure(script):
8 | - if you use vim or emacs, stick with that
9 | - otherwise, we recommend Cursive: https://cursive-ide.com/ (over Lighttable, SublimeText, Atom, etc.)
10 |
11 |
12 | Fork the Project
13 |
14 | Clone your fork locally
15 |
16 | Install Packages
17 |
18 |
19 | [Getting Started](../dev/getting-up-and-running-in-development.md)
20 |
21 |
22 |
23 |
24 | let us know what features you plan to work on
25 | PR early, to start a discussion (and avoid major refactoring)
26 |
27 |
28 | Issues
29 |
30 | https://github.com/braidchat/braid/issues
31 |
32 | Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug
33 |
34 | There is a [Help Wanted](https://github.com/braidchat/braid/issues?q=is%3Aissue+is%3Aopen+label%3Ahelp-wanted) tag for issues that should be ideal for people who are not very familiar with the codebase yet.
35 |
36 |
37 | ## Support
38 |
39 | If at any point, you run into problems, jump into the [braid group](http://braid.chat/group/braid).
40 |
41 | @jamesnvc and @rafd have a standing offer to remotely pair-code with anyone on Braid (via [Screenhero]() or [Teamviewer]()). Message us if you'd like to take us up on it.
42 |
43 |
44 |
45 |
46 |
47 | Feature Branches
48 |
49 | `git checkout -b new-shiny-thing`
50 |
51 |
52 |
53 | Commit Best Practices
54 |
55 | - messages: chris.beams.io/posts/git-commit/
56 | -
57 |
58 |
59 | Pull in Upstream Change Regularly
60 |
61 | ```
62 | git remote add upstream ...
63 | git fetch upstream
64 | git merge upstream/master --ff
65 | ```
66 |
67 |
68 | Make a Pull Request
69 |
70 | ```
71 | git push origin new-shiny-thing
72 | ```
73 |
74 |
75 |
76 | Add yourself to [CONTRIBUTORS.md](../CONTRIBUTORS.edn)
77 |
78 |
79 |
--------------------------------------------------------------------------------
/docs/drafts/developing-modules.md:
--------------------------------------------------------------------------------
1 | # Developing Braid Modules
2 |
3 | ## Module Spec
4 |
5 | - a module is self-contained in a folder directly under `braid`, ie. `src/braid/widget`
6 |
7 | `braid.widget.core`
8 |
9 | - a module must have a `braid.widget.core` namespace
10 | - `braid.widget.core` must be a `cljc` file
11 | - `braid.widget.core` must have a namespace docstring explaining the purpose of the module
12 | - `braid.widget.core` must have a non-private `init!` function
13 | - `braid.widget.core` can contain implementation details or require functions or values from other namespaces
14 | - all other functions in `braid.widget.core` (other than `init!`) should be private
15 |
16 | `braid.widget.api`
17 |
18 | - a module can optionally expose functionality to other modules via a `braid.widget.api` namespace
19 | - `braid.widget.api` can be a `clj`, `cljs` or `cljc` file
20 | - `braid.widget.api` should only contain functions (or other values) intended for use by other modules (all "private" values should be in other namespaces under `braid.widget.*`)
21 | - all public functions should have docstrings
22 | - all public functions should validate their inputs (for example using `:pre` conditions and spec)
23 |
24 |
25 |
26 | ## Development Notes
27 |
28 | - new modules need to be added to `braid.core.modules`
29 | - most "new functionality" should be implemented by creating new modules rather than changing exist modules
30 | - you may need to expose functionality in an existing module to achieve what you need
31 | - you probably want to make use of `braid.core.hooks`
32 |
33 |
34 | ## Notes for Future
35 |
36 | - a way for each module to declare library dependencies
37 | - a way for each module to declare tests
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/docs/drafts/guidelines-code.md:
--------------------------------------------------------------------------------
1 | ## Branching Model
2 |
3 | ## Commit Messages
4 |
5 | ## Code Organization
6 |
7 | Tests for server
8 |
9 |
10 |
11 | We value simplicity and elegance.
12 |
13 |
14 |
15 | Three audiences:
16 | - contributors
17 | no-configuration start
18 |
19 | - personal server deploy
20 |
21 |
22 | - production deployment
23 | possible to swap out components, scale horizontally and vertically
24 |
25 |
26 |
27 |
28 | development production
29 | database datomic free datomic pro
30 | session store in-memory redis
31 | search datomic onyx + solr
32 |
33 |
34 |
35 |
36 | ## Nomenclature
37 |
38 | tags
39 |
40 | threads/conversations
41 |
--------------------------------------------------------------------------------
/docs/drafts/guidelines-design.md:
--------------------------------------------------------------------------------
1 | # Design Guidelines
2 |
3 | ...when possible, use analogies (ex. to email)
4 |
5 | every action should give immediate visual feedback
6 |
7 |
8 |
--------------------------------------------------------------------------------
/docs/drafts/guidelines-documentation.md:
--------------------------------------------------------------------------------
1 | # Documentation Guidelines
2 |
--------------------------------------------------------------------------------
/docs/images/braid-icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braidchat/braid/5464e39ce5ce8d0fda3b1a614715ee4c7cbafe06/docs/images/braid-icon-256.png
--------------------------------------------------------------------------------
/docs/images/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braidchat/braid/5464e39ce5ce8d0fda3b1a614715ee4c7cbafe06/docs/images/screenshot.png
--------------------------------------------------------------------------------
/docs/images/youtube-player.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braidchat/braid/5464e39ce5ce8d0fda3b1a614715ee4c7cbafe06/docs/images/youtube-player.png
--------------------------------------------------------------------------------
/docs/responsible-disclosure-policy.md:
--------------------------------------------------------------------------------
1 | # Responsible Disclosure Policy
2 |
3 | Safety and data security is very important to the Braid community. If you have discovered a security vulnerability in our code base, we appreciate your help in disclosing it to us in a responsible manner:
4 |
5 | 1. Please email security@braidchat.com to report any security vulnerabilities found in the [open source code maintained by Braid](https://github.com/braidchat/).
6 | 2. Please refrain from requesting compensation for reporting vulnerabilities.
7 | 3. We will acknowledge receipt of your vulnerability report and send you regular updates about our progress.
8 | 4. If your report results in a change to the code base or documentation of a Braid product, we will – at your option – publicly acknowledge your responsible disclosure.
9 |
10 |
11 | Please do not test directly against our production instances. You can [install a copy of Braid yourself](./dev/getting-up-and-running-in-development.md) to test against.
12 |
13 | If you want to encrypt your disclosure email please email us to ask for our PGP key.
14 |
--------------------------------------------------------------------------------
/gateway.cljs.edn:
--------------------------------------------------------------------------------
1 | {:main braid.core.client.gateway.core
2 | :closure-defines {"re_frame.trace.trace_enabled_QMARK_" true}
3 | :asset-path "/js/dev/gateway/"
4 | :output-to "resources/public/js/dev/gateway.js"
5 | :output-dir "resources/public/js/dev/gateway/"
6 | :optimizations :none
7 | :verbose false}
8 |
--------------------------------------------------------------------------------
/prod.cljs.edn:
--------------------------------------------------------------------------------
1 | ^{:watch-dirs ["src"]}
2 | {:asset-path "/js/prod/"
3 | :output-dir "resources/public/js/prod/out"
4 | :optimizations :advanced
5 | :pretty-print false
6 | :elide-asserts true
7 | :closure-defines {goog.DEBUG false}
8 | :modules {:cljs-base
9 | {:output-to "resources/public/js/prod/base.js"}
10 | :desktop
11 | {:output-to "resources/public/js/prod/desktop.js"
12 | :entries #{"braid.core.client.desktop.core"}}
13 | :gateway
14 | {:output-to "resources/public/js/prod/gateway.js"
15 | :entries #{"braid.core.client.gateway.core"}}}
16 | :verbose true}
17 |
--------------------------------------------------------------------------------
/profiles.sample.clj:
--------------------------------------------------------------------------------
1 | ; Rename this file to profiles.clj
2 | ; Run `lein with-profile +braid repl`
3 |
4 | {:braid
5 | {:env
6 | {;; for invite emails:
7 | :email-host "smtp.mailgun.org"
8 | :email-user "myuser@site.com"
9 | :email-password "my_email_password"
10 | :email-port 587
11 | :email-secure "tls"
12 | :email-from "noreply@braid.chat"
13 | :site-url "http://localhost:5555"
14 | :hmac-secret "foobar"
15 | ;; for avatar and file uploads
16 | :aws-bucket "braid-bucket"
17 | :aws-region "us-east-1"
18 | :aws-access-key "my_aws_key"
19 | :aws-secret-key "my_aws_secret"
20 | ;; for github login
21 | :github-client-id "..."
22 | :github-client-secret "..."
23 | :google-maps-api-key ""
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/resources/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ../CHANGELOG.md
--------------------------------------------------------------------------------
/resources/public/desktop.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{app_title}}
5 |
6 |
7 |
8 |
9 | {{#extra_scripts}}
10 | {{& script}}
11 | {{/extra_scripts}}
12 | {{#prod}}
13 |
14 |
15 | {{/prod}}
16 | {{#dev}}
17 |
18 | {{/dev}}
19 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/resources/public/fonts/fa-brands-400.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braidchat/braid/5464e39ce5ce8d0fda3b1a614715ee4c7cbafe06/resources/public/fonts/fa-brands-400.eot
--------------------------------------------------------------------------------
/resources/public/fonts/fa-brands-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braidchat/braid/5464e39ce5ce8d0fda3b1a614715ee4c7cbafe06/resources/public/fonts/fa-brands-400.ttf
--------------------------------------------------------------------------------
/resources/public/fonts/fa-brands-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braidchat/braid/5464e39ce5ce8d0fda3b1a614715ee4c7cbafe06/resources/public/fonts/fa-brands-400.woff
--------------------------------------------------------------------------------
/resources/public/fonts/fa-brands-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braidchat/braid/5464e39ce5ce8d0fda3b1a614715ee4c7cbafe06/resources/public/fonts/fa-brands-400.woff2
--------------------------------------------------------------------------------
/resources/public/fonts/fa-solid-900.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braidchat/braid/5464e39ce5ce8d0fda3b1a614715ee4c7cbafe06/resources/public/fonts/fa-solid-900.eot
--------------------------------------------------------------------------------
/resources/public/fonts/fa-solid-900.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braidchat/braid/5464e39ce5ce8d0fda3b1a614715ee4c7cbafe06/resources/public/fonts/fa-solid-900.ttf
--------------------------------------------------------------------------------
/resources/public/fonts/fa-solid-900.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braidchat/braid/5464e39ce5ce8d0fda3b1a614715ee4c7cbafe06/resources/public/fonts/fa-solid-900.woff
--------------------------------------------------------------------------------
/resources/public/fonts/fa-solid-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braidchat/braid/5464e39ce5ce8d0fda3b1a614715ee4c7cbafe06/resources/public/fonts/fa-solid-900.woff2
--------------------------------------------------------------------------------
/resources/public/gateway.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{app_title}}
5 |
6 |
7 |
8 |
9 | {{#prod}}
10 |
11 |
12 | {{/prod}}
13 | {{#dev}}
14 |
15 | {{/dev}}
16 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/resources/public/images/braid-logo-color.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/resources/public/images/braid.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/resources/public/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braidchat/braid/5464e39ce5ce8d0fda3b1a614715ee4c7cbafe06/resources/public/images/favicon.png
--------------------------------------------------------------------------------
/resources/public/images/quests/conversation-close.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braidchat/braid/5464e39ce5ce8d0fda3b1a614715ee4c7cbafe06/resources/public/images/quests/conversation-close.gif
--------------------------------------------------------------------------------
/resources/public/images/quests/conversation-new.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braidchat/braid/5464e39ce5ce8d0fda3b1a614715ee4c7cbafe06/resources/public/images/quests/conversation-new.gif
--------------------------------------------------------------------------------
/resources/public/images/quests/conversation-reply.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/braidchat/braid/5464e39ce5ce8d0fda3b1a614715ee4c7cbafe06/resources/public/images/quests/conversation-reply.gif
--------------------------------------------------------------------------------
/resources/public/oauth-post-auth.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/resources/templates/email_digest.html.mustache:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Braid Email Digest
5 |
34 |
35 |
36 | Braid Updates
37 |
38 |
39 |
40 |
41 |
42 |
45 |
46 | {{#threads}}
47 |
48 |
49 |
50 | {{#messages}}
51 |
52 | 
53 |
54 | {{sender}}
55 | {{created-at}}
56 |
57 | {{content}}
58 |
59 | {{/messages}}
60 |
61 |
62 |
63 |
64 | {{/threads}}
65 |
66 | |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/resources/templates/email_digest.txt.mustache:
--------------------------------------------------------------------------------
1 | Braid Updates
2 |
3 | {{#threads}}
4 |
5 | {{#messages}}
6 | - {{sender}}: {{content}}
7 | {{/messages}}
8 |
9 | {{/threads}}
10 |
--------------------------------------------------------------------------------
/resources/templates/reset_page.html.mustache:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Reset Password
5 |
6 |
7 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/scripts/video-convert.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # This script converts movie files into gifs.
4 | # It is used when creating instruction videos for quests.
5 | #
6 | # How to create a quest instruction videos:
7 | #
8 | # 0. Install dependencies:
9 | #
10 | # on Mac: brew install ffmpeg gifsicle
11 | #
12 | # 1. Capture a video
13 | #
14 | # on Mac: you can use Quicktime Player: File > New Screen Recording
15 | #
16 | # 2. Convert the video to a gif using the script:
17 | #
18 | # ./video-convert.sh my-video.mov
19 | #
20 | # (this creates a my-video.gif)
21 | #
22 | # 3. Move the gif to /resource/public/images/quests/
23 | #
24 | # 4. Add the image path to `braid.quests.list`
25 | #
26 | # For Linux, consider using byzanz: http://linux.die.net/man/1/byzanz-record
27 | #
28 | # Code based on http://blog.pkh.me/p/21-high-quality-gif-with-ffmpeg.html
29 |
30 | INFILE=$1
31 | outfile="${INFILE%%.mov}.gif"
32 |
33 | filters="fps=15,scale=800:-1:flags=lanczos"
34 | palette="/tmp/palette.png"
35 |
36 | ffmpeg -v warning -i $1 -vf "$filters,palettegen" -y $palette
37 | ffmpeg -v warning -i $1 -r 10 -i $palette -lavfi "$filters [x]; [x][1:v] paletteuse" -f gif - | gifsicle --optimize=3 --delay=4 > $outfile
38 |
39 |
--------------------------------------------------------------------------------
/src/braid/base/client/pages.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.base.client.pages
2 | (:require
3 | [spec-tools.data-spec :as ds]
4 | [braid.core.hooks :as hooks]))
5 |
6 | (def page-dataspec
7 | {:key keyword?
8 | :view fn?
9 | (ds/opt :on-load) fn?
10 | (ds/opt :on-exit) fn?
11 | (ds/opt :styles) vector?})
12 |
13 | (defonce pages
14 | (hooks/register! (atom {}) {keyword? page-dataspec}))
15 |
16 | (defn on-load! [page-id page]
17 | (when-let [f (get-in @pages [page-id :on-load])]
18 | (f page)))
19 |
20 | (defn on-exit! [page-id page]
21 | (when-let [f (get-in @pages [page-id :on-exit])]
22 | (f page)))
23 |
24 | (defn get-view [page-id]
25 | (get-in @pages [page-id :view]))
26 |
--------------------------------------------------------------------------------
/src/braid/base/client/remote_handlers.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.base.client.remote-handlers
2 | (:require
3 | [braid.core.hooks :as hooks]
4 | [braid.base.client.socket :as socket]
5 | [taoensso.timbre :as timbre :refer-macros [errorf]]))
6 |
7 | (defonce incoming-socket-message-handlers
8 | (hooks/register! (atom {}) {keyword? fn?}))
9 |
10 | (defmethod socket/event-handler :default
11 | [[id data]]
12 | (if-let [handler (@incoming-socket-message-handlers id)]
13 | (handler id data)
14 | (errorf "No socket message handler for id: %s" id)))
15 |
--------------------------------------------------------------------------------
/src/braid/base/client/root_view.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.base.client.root-view
2 | (:require
3 | [braid.core.hooks :as hooks]))
4 |
5 | (defonce root-views (hooks/register! (atom []) [fn?]))
6 |
7 |
--------------------------------------------------------------------------------
/src/braid/base/client/router.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.base.client.router
2 | (:require
3 | [accountant.core :as accountant]
4 | [secretary.core :as secretary])
5 | (:import
6 | (goog Uri)))
7 |
8 | (defn init []
9 | (accountant/configure-navigation!
10 | {:nav-handler (fn [path]
11 | (secretary/dispatch! path))
12 | :path-exists? (fn [path]
13 | (secretary/locate-route path))}))
14 |
15 | (defn dispatch-current-path! []
16 | (accountant/dispatch-current!))
17 |
18 | (defn go-to [path]
19 | (accountant/navigate! path))
20 |
21 | (defn current-path []
22 | (.getPath (.parse Uri js/window.location)))
23 |
--------------------------------------------------------------------------------
/src/braid/base/client/state.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.base.client.state
2 | (:require
3 | [clojure.spec.alpha :as s]
4 | [spec-tools.data-spec :as ds]
5 | [re-frame.core :as re-frame]
6 | [braid.core.common.util :as util]))
7 |
8 | (defn initialize-state
9 | [db]
10 | (-> (db ::initial-state)
11 | (merge (select-keys db [::state-spec
12 | ::initial-state]))))
13 |
14 | (re-frame/reg-event-fx ::register-state!
15 | (fn [{db :db} [_ state spec]]
16 | {:db (-> db
17 | (update ::initial-state merge state)
18 | (update ::state-spec merge spec))}))
19 |
20 | (defn register-state!
21 | [state spec]
22 | ;; Dispatch sync because we want the module setup calls
23 | ;; to finish before initializing the db
24 | (re-frame/dispatch-sync [::register-state! state spec]))
25 |
26 | (re-frame/reg-sub :braid.state/valid?
27 | (fn [db _]
28 | (util/valid? (db ::state-spec) db)))
29 |
30 | (def validate-schema-interceptor
31 | (re-frame/after
32 | (fn [db [event-id]]
33 | (when-let [errors (s/explain-data
34 | (ds/spec {:name ::app-state
35 | :spec (db ::state-spec)})
36 | db)]
37 | (js/console.error
38 | (str
39 | "Event " event-id
40 | " caused the state to be invalid:\n")
41 | (pr-str (map (fn [problem]
42 | {:path (problem :path)
43 | :pred (problem :pred)})
44 | (::s/problems errors))))))))
45 |
46 | (if ^boolean goog.DEBUG
47 | (defn reg-event-fx
48 | ([id handler-fn]
49 | (reg-event-fx id nil handler-fn))
50 | ([id interceptors handler-fn]
51 | (re-frame/reg-event-fx
52 | id
53 | [validate-schema-interceptor
54 | interceptors]
55 | handler-fn)))
56 | (def reg-event-fx re-frame/reg-event-fx))
57 |
--------------------------------------------------------------------------------
/src/braid/base/client/styles.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.base.client.styles
2 | (:require
3 | [braid.core.hooks :as hooks]
4 | [garden.core :refer [css]]))
5 |
6 | (def style-dataspec
7 | #(or (vector? %) (map? %)))
8 |
9 | (defonce module-styles
10 | (hooks/register!
11 | (atom [] [style-dataspec])))
12 |
13 | (defn styles-view []
14 | [:style
15 | {:type "text/css"
16 | :dangerouslySetInnerHTML
17 | {:__html (css
18 | {:auto-prefix #{:transition
19 | :flex-direction
20 | :flex-shrink
21 | :align-items
22 | :animation
23 | :flex-grow}
24 | :vendors ["webkit"]}
25 |
26 | @module-styles)}}])
27 |
--------------------------------------------------------------------------------
/src/braid/base/client/subs.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.base.client.subs
2 | (:require
3 | [re-frame.core :refer [reg-sub reg-sub-raw]]))
4 |
5 | (defn register-subs!
6 | [sub-map]
7 | (doseq [[k f] sub-map]
8 | (reg-sub k f)))
9 |
10 | (defn register-subs-raw!
11 | [sub-map]
12 | (doseq [[k f] sub-map]
13 | (reg-sub-raw k f)))
14 |
--------------------------------------------------------------------------------
/src/braid/base/conf.clj:
--------------------------------------------------------------------------------
1 | (ns braid.base.conf
2 | (:require
3 | [braid.base.conf-extra :refer [ports-config]]
4 | [braid.core.hooks :as hooks]
5 | [malli.core :as malli]
6 | [malli.error :as malli.error]
7 | [malli.transform :as malli.transform]
8 | [clojure.pprint :as pprint]
9 | [mount.core :as mount :refer [defstate]]))
10 |
11 | (defonce config-vars
12 | (hooks/register! (atom []) [any?]))
13 |
14 | (defn ->malli-schema
15 | [config-vars]
16 | (->> config-vars
17 | (map (fn [{:keys [key required? schema]}]
18 | [key {:optional (not required?)} schema]))
19 | (into [:map])))
20 |
21 | (defn select-and-validate
22 | "Extracts and validates keys from a given env map according to config-vars.
23 | Removes keys that aren't defined in schema.
24 | Throws an error when missing keys or values are invalid."
25 | [env config-vars]
26 | (let [schema (->malli-schema config-vars)
27 | env (malli/decode schema env (malli.transform/transformer
28 | malli.transform/strip-extra-keys-transformer
29 | malli.transform/string-transformer))]
30 | (if (malli/validate schema env)
31 | env
32 | (let [cause (malli/explain schema env)]
33 | (throw
34 | (ex-info (str "Config invalid\n"
35 | (with-out-str
36 | (pprint/pprint (malli.error/humanize cause)))) cause))))))
37 |
38 | #_(select-and-validate
39 | {:foo "123"
40 | :bar 2}
41 | [{:key :foo
42 | :required? false
43 | :schema [:int]}])
44 |
45 |
46 | (defstate config
47 | :start
48 | (merge ;; temp defaults
49 | ;; TODO don't special case these here
50 | ;; ports should come from config
51 | {:db-url "datomic:mem://braid"
52 | :site-url (str "http://localhost:" (:port (mount/args)))
53 | :hmac-secret "secret"
54 | :app-title "Braid"}
55 | @ports-config ; overrides site url when port is automatically found
56 | (select-and-validate (mount/args) @config-vars)))
57 |
--------------------------------------------------------------------------------
/src/braid/base/conf_extra.clj:
--------------------------------------------------------------------------------
1 | (ns braid.base.conf-extra)
2 |
3 | (defonce ports-config (atom {}))
4 |
--------------------------------------------------------------------------------
/src/braid/base/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.base.core
2 | (:require
3 | [braid.base.api :as base]
4 | #?@(:cljs
5 | [[braid.base.client.router :as router]
6 | [braid.base.client.events :as events]
7 | [braid.base.client.socket :as socket]
8 | [re-frame.core :refer [dispatch]]]
9 | :clj
10 | [[braid.base.server.initial-data :as initial-data]])))
11 |
12 | (defn init! []
13 | #?(:cljs
14 | (do
15 | (base/register-incoming-socket-message-handlers!
16 | {::init-data
17 | (fn [_ data]
18 | (dispatch [::set-init-data! data])
19 | (router/dispatch-current-path!)
20 | (dispatch [:notify-if-client-out-of-date! (data :version-checksum)]))
21 |
22 | :socket/connected
23 | (fn [_ _]
24 | (socket/chsk-send! [::server-start nil]))})
25 |
26 | (base/register-events!
27 | {::set-init-data!
28 | (fn [{state :db} [_ data]]
29 | ;; FIXME :set-login-state! defined in chat
30 | {:dispatch-n (list [:set-login-state! :app])
31 | :db (-> state
32 | (as-> <>
33 | (reduce (fn [db f] (f db data))
34 | <>
35 | @events/initial-user-data-handlers)))})}))
36 |
37 | :clj
38 | (do
39 | ;; TODO transfer appropriate vars from braid.chat.core
40 | (base/register-config-var! :app-title :optional [:string])
41 | (base/register-config-var! :prod-js :optional [:boolean])
42 | (base/register-config-var! :redis-uri :optional [:re #"^redis://.*$"])
43 |
44 |
45 | (base/register-server-message-handlers!
46 | {::server-start
47 | (fn [{:keys [user-id]}]
48 | {:chsk-send! [user-id [::init-data
49 | (->> @initial-data/initial-user-data
50 | (into {} (map (fn [f] (f user-id)))))]]})}))))
51 |
--------------------------------------------------------------------------------
/src/braid/base/server/cache.clj:
--------------------------------------------------------------------------------
1 | (ns braid.base.server.cache
2 | (:require
3 | [braid.base.conf :as conf]
4 | [taoensso.carmine :as car]))
5 |
6 | ; same as conf in handler, but w/e
7 | (def redis-conn (delay
8 | {:pool {}
9 | :spec {:uri (conf/config :redis-uri)}}))
10 |
11 | (def redis? (delay (conf/config :redis-uri)))
12 |
13 | (def dev-cache
14 | "Cache used in place of redis when running in dev/demo mode"
15 | (atom {}))
16 |
17 | (defn cache-set! [k v]
18 | (if @redis?
19 | (car/wcar @redis-conn (car/set k v))
20 | (swap! dev-cache assoc k v)))
21 |
22 | (defn cache-get [k]
23 | (if @redis?
24 | (car/wcar @redis-conn (car/get k))
25 | (@dev-cache k)))
26 |
27 | (defn cache-del! [k]
28 | (if @redis?
29 | (car/wcar @redis-conn (car/del k))
30 | (swap! dev-cache dissoc k)))
31 |
--------------------------------------------------------------------------------
/src/braid/base/server/cqrs.clj:
--------------------------------------------------------------------------------
1 | (ns braid.base.server.cqrs
2 | (:require
3 | [tada.events.core :as tada]
4 | [braid.core.hooks :as hooks]
5 | [taoensso.timbre :as timbre]))
6 |
7 | (defn dispatch [command-id args]
8 | (tada/do! command-id args))
9 |
10 | (defonce commands (hooks/register! (atom [])))
11 |
12 | (defn ->ws-handler [command]
13 | (fn [{:keys [user-id ?data ?reply-fn]}]
14 | (timbre/debug "Command:" (:id command) ?data)
15 | (try
16 | (dispatch (:id command)
17 | (assoc ?data
18 | :user-id user-id))
19 | (?reply-fn :braid/ok)
20 | (catch Exception e
21 | (timbre/warn "Command Error:" e)
22 | (?reply-fn {:cqrs/error {:message (.getMessage e)
23 | :data (ex-data e)}})))
24 | ;; returning {} here, b/c our socket-message-handlers
25 | ;; expect a map of cofx
26 | {}))
27 |
28 | (defn update-registry! []
29 | (tada/register! @commands))
30 |
--------------------------------------------------------------------------------
/src/braid/base/server/cqrs_fx.clj:
--------------------------------------------------------------------------------
1 | (ns braid.base.server.cqrs-fx
2 | "Namespace that collects common CQRS command fx"
3 | (:require
4 | [braid.base.server.socket :as socket]
5 | [braid.core.server.sync-helpers :as helpers]
6 | [braid.core.server.db :as db]
7 | [braid.base.server.cqrs :as cqrs]))
8 |
9 | (def run-txns! db/run-txns!)
10 |
11 | (def chsk-send! socket/chsk-send!)
12 |
13 | (def group-broadcast! helpers/broadcast-group-change)
14 |
15 | (def dispatch! cqrs/dispatch)
16 |
--------------------------------------------------------------------------------
/src/braid/base/server/http_client_routes.clj:
--------------------------------------------------------------------------------
1 | (ns braid.base.server.http-client-routes
2 | (:require
3 | [braid.base.conf :refer [config]]
4 | [compojure.core :refer [GET defroutes]]
5 | [compojure.route :refer [resources]]
6 | [ring.util.response :refer [resource-response]]))
7 |
8 | (defroutes resource-routes
9 |
10 | ; add cache-control headers to js files)
11 | ; (since it uses a cache-busted url anyway)
12 | (GET (str "/js/:build{desktop|gateway|base}.js") [build]
13 | (if (boolean (config :prod-js))
14 | (if-let [response (resource-response (str "public/js/prod/" build ".js"))]
15 | (assoc-in response [:headers "Cache-Control"] "max-age=365000000, immutable")
16 | {:status 404
17 | :body "File Not Found"})
18 | (if-let [response (resource-response (str "public/js/dev/" build ".js"))]
19 | response
20 | {:status 200
21 | :headers {"Content-Type" "application/javascript"}
22 | :body (str "alert('The " build " js files are missing. Please compile them with cljsbuild or figwheel.');")})))
23 |
24 | (resources "/"))
25 |
--------------------------------------------------------------------------------
/src/braid/base/server/initial_data.clj:
--------------------------------------------------------------------------------
1 | (ns braid.base.server.initial-data
2 | (:require
3 | [braid.core.hooks :as hooks]))
4 |
5 | (defonce initial-user-data (hooks/register! (atom [])))
6 |
--------------------------------------------------------------------------------
/src/braid/base/server/jobs.clj:
--------------------------------------------------------------------------------
1 | (ns braid.base.server.jobs
2 | (:require
3 | [braid.core.hooks :as hooks]
4 | [braid.base.server.scheduler :refer [scheduler]]
5 | [clojurewerkz.quartzite.jobs :as j :refer [defjob]]
6 | [clojurewerkz.quartzite.schedule.cron :as cron]
7 | [clojurewerkz.quartzite.scheduler :as qs]
8 | [clojurewerkz.quartzite.triggers :as t]
9 | [mount.core :refer [defstate]]
10 | [taoensso.timbre :as timbre]))
11 |
12 | (defonce daily-module-jobs (hooks/register! (atom [])))
13 |
14 | (defjob DailyModulesJob
15 | [ctx]
16 | (timbre/debugf "Starting daily modules job")
17 | (doseq [job @daily-module-jobs]
18 | (try
19 | (job)
20 | (catch Exception e
21 | (timbre/errorf "Error running job %s: %s" job e)))))
22 |
23 | (defn daily-modules-job
24 | []
25 | (j/build
26 | (j/of-type DailyModulesJob)
27 | (j/with-identity (j/key "jobs.daily-modules.1"))))
28 |
29 | (defn daily-modules-trigger
30 | []
31 | (t/build
32 | (t/with-identity (t/key "triggers.daily-modules"))
33 | (t/start-now)
34 | (t/with-schedule
35 | (cron/schedule
36 | (cron/daily-at-hour-and-minute 2 30)))))
37 |
38 | (defn register-daily-job!
39 | [job-fn]
40 | (swap! daily-module-jobs conj job-fn))
41 |
42 | (defn add-jobs
43 | [scheduler]
44 | (qs/schedule scheduler (daily-modules-job) (daily-modules-trigger)))
45 |
46 | (defn remove-jobs
47 | [scheduler]
48 | (qs/delete-job scheduler (j/key "jobs.daily-modules.1")))
49 |
50 | (defstate jobs
51 | :start (add-jobs scheduler)
52 | :stop (remove-jobs scheduler))
53 |
--------------------------------------------------------------------------------
/src/braid/base/server/scheduler.clj:
--------------------------------------------------------------------------------
1 | (ns braid.base.server.scheduler
2 | (:require
3 | [clojurewerkz.quartzite.scheduler :as qs]
4 | [mount.core :as mount :refer [defstate]]
5 | [taoensso.timbre :as timbre]))
6 |
7 | (defn stop-scheduler!
8 | [s]
9 | (qs/shutdown s))
10 |
11 | (defn start-scheduler!
12 | []
13 | (timbre/infof "starting quartz scheduler")
14 | (doto (qs/initialize)
15 | (qs/start)))
16 |
17 | (defstate scheduler
18 | :start (start-scheduler!)
19 | :stop (stop-scheduler! scheduler))
20 |
--------------------------------------------------------------------------------
/src/braid/base/server/schema.clj:
--------------------------------------------------------------------------------
1 | (ns braid.base.server.schema
2 | (:require
3 | [clojure.spec.alpha :as s]
4 | [spec-tools.data-spec :as ds]
5 | [braid.core.hooks :as hooks]))
6 |
7 | (def rule-dataspec
8 | {:db/ident keyword?
9 | (ds/opt :db/doc) string?
10 | :db/valueType (s/spec #{:db.type/boolean
11 | :db.type/instant
12 | :db.type/keyword
13 | :db.type/long
14 | :db.type/ref
15 | :db.type/string
16 | :db.type/uuid})
17 | :db/cardinality (s/spec #{:db.cardinality/one
18 | :db.cardinality/many})
19 | (ds/opt :db/unique) (s/spec #{:db.unique/identity
20 | :db.unique/value})})
21 |
22 | (defonce schema
23 | (hooks/register! (atom []) [rule-dataspec]))
24 |
--------------------------------------------------------------------------------
/src/braid/base/server/seed.clj:
--------------------------------------------------------------------------------
1 | (ns braid.base.server.seed
2 | (:require
3 | [braid.core.hooks :as hooks]))
4 |
5 | (defonce seed-fns (hooks/register! (atom [])))
6 |
7 | (defn seed! []
8 | (when (empty? @seed-fns)
9 | "Nothing seeded. You may need to initialize modules first.")
10 | (doseq [f @seed-fns]
11 | (f)))
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/braid/base/server/socket.clj:
--------------------------------------------------------------------------------
1 | (ns braid.base.server.socket
2 | (:require
3 | [mount.core :as mount :refer [defstate]]
4 | [taoensso.sente.server-adapters.http-kit :refer [get-sch-adapter]]
5 | [taoensso.sente.packers.transit :as sente-transit]
6 | [taoensso.sente :as sente]))
7 |
8 | (let [packer (sente-transit/get-transit-packer :json)
9 | {:keys [ch-recv send-fn ajax-post-fn ajax-get-or-ws-handshake-fn
10 | connected-uids]}
11 | (sente/make-channel-socket! (get-sch-adapter)
12 | {:user-id-fn
13 | (fn [ob] (or (get-in ob [:session :user-id])
14 | (get-in ob [:session :fake-id])))
15 | :simple-auto-threading? true
16 | :csrf-token-fn nil
17 | :ws-kalive-ms 30000
18 | :packer packer})]
19 | (def ring-ajax-post ajax-post-fn)
20 | (def ring-ajax-get-or-ws-handshake ajax-get-or-ws-handshake-fn)
21 | (def ch-chsk ch-recv)
22 | (def chsk-send! send-fn)
23 | (def connected-uids connected-uids))
24 |
--------------------------------------------------------------------------------
/src/braid/base/server/spa.clj:
--------------------------------------------------------------------------------
1 | (ns braid.base.server.spa
2 | (:require
3 | [cljstache.core :as cljstache]
4 | [braid.lib.s3 :as s3]
5 | [braid.core.hooks :as hooks]
6 | [braid.base.conf :refer [config]]
7 | [braid.lib.digest :as digest]))
8 |
9 | (def additional-script-dataspec
10 | (fn [tag]
11 | (or (fn? tag)
12 | (and (map? tag)
13 | (or (:src tag) (:body tag))))))
14 |
15 | (defonce additional-scripts
16 | (hooks/register! (atom []) [additional-script-dataspec]))
17 |
18 | (defn init-script [client]
19 | (str "braid.core.client." client ".core.init();"))
20 |
21 | (defn get-html [client vars]
22 | (let [prod-js? (boolean (config :prod-js))]
23 | (cljstache/render-resource
24 | (str "public/" client ".html")
25 | (merge {:prod prod-js?
26 | :dev (not prod-js?)
27 | :algo "sha256"
28 | :init_script (init-script client)
29 | :js (if prod-js?
30 | (str (digest/from-file (str "public/js/prod/" client ".js")))
31 | (str (digest/from-file (str "public/js/dev/" client ".js"))))
32 | :basejs (when prod-js?
33 | (str (digest/from-file (str "public/js/prod/base.js"))))
34 | :app_title (config :app-title)
35 | :asset_domain (s3/s3-host config)
36 | :extra_scripts
37 | (->> @additional-scripts
38 | (map (fn [s] (if (fn? s) (s) s)))
39 | (map (fn [s]
40 | {:script (if-let [src (:src s)]
41 | (str "")
42 | (str ""))})))}
43 | vars))))
44 |
--------------------------------------------------------------------------------
/src/braid/bots/client/autocomplete.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.bots.client.autocomplete
2 | (:require
3 | [braid.lib.upload :as upload]
4 | [braid.core.client.ui.views.autocomplete :refer [fuzzy-matches?]]
5 | [clojure.string :as string]
6 | [re-frame.core :refer [subscribe dispatch]]))
7 |
8 | ; / -> autocompletes bots
9 | (defn bots-autocomplete-engine [text]
10 | (let [pattern #"^/(\w+)$"
11 | open-group (subscribe [:open-group-id])]
12 | (when-let [bot-name (second (re-find pattern text))]
13 | (into ()
14 | (comp (filter (fn [b] (fuzzy-matches? (b :nickname) bot-name)))
15 | (map (fn [b]
16 | {:key (constantly (b :id))
17 | :action (fn [])
18 | :message-transform
19 | (fn [text]
20 | (string/replace text pattern
21 | (str "/" (b :nickname) " ")))
22 | :html
23 | (constantly
24 | [:div.bot.match
25 | [:img.avatar {:src (upload/->path (b :avatar))}]
26 | [:div.info
27 | [:div.name (b :nickname)]
28 | [:div.extra]]])})))
29 | @(subscribe [:bots/group-bots] [open-group])))))
30 |
--------------------------------------------------------------------------------
/src/braid/bots/client/remote_handlers.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.bots.client.remote-handlers
2 | (:require
3 | [re-frame.core :refer [dispatch]]))
4 |
5 | (def handlers
6 | {:braid.client.bots/new-bot
7 | (fn [_ [group-id bot]]
8 | (dispatch [:bots/add-group-bot! [group-id bot]]))
9 |
10 | :braid.client.bots/retract-bot
11 | (fn [_ [group-id bot-id]]
12 | (dispatch [:bots/remove-group-bot! [group-id bot-id]]))
13 |
14 | :braid.client.bots/edit-bot
15 | (fn [_ [group-id bot]]
16 | (dispatch [:bots/update-group-bot! [group-id bot]]))})
17 |
--------------------------------------------------------------------------------
/src/braid/bots/client/subs.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.bots.client.subs
2 | (:refer-clojure :exclude [subs])
3 | (:require
4 | [re-frame.core :refer [reg-sub]]))
5 |
6 | (def subs
7 | {:bots/group-bots
8 | (fn [state _ [group-id]]
9 | (get-in state [:groups group-id :bots]))
10 |
11 | :bots/group-bot
12 | (fn [{:keys [open-group-id] :as state} [_ bot-user-id]]
13 | (let [group-bots (get-in state [:groups open-group-id :bots])]
14 | (some-> group-bots
15 | (->> (filter (fn [b] (= (b :user-id) bot-user-id))))
16 | first)))})
17 |
--------------------------------------------------------------------------------
/src/braid/bots/client/views/bot_sender_view.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.bots.client.views.bot-sender-view
2 | (:require
3 | [braid.lib.upload :as upload]
4 | [braid.lib.color :as color]
5 | [braid.core.client.routes :as routes]
6 | [re-frame.core :refer [subscribe]]))
7 |
8 | (defn sender-view
9 | [bot-id]
10 | (let [sender @(subscribe [:bots/group-bot bot-id])
11 | current-group (subscribe [:open-group-id])
12 | sender-path (routes/group-page-path {:group-id @current-group
13 | :page-id "bots"})]
14 | (when sender
15 | {:avatar [:a.avatar {:href sender-path
16 | :tab-index -1}
17 | [:img {:src (upload/->path (:avatar sender))
18 | :style {:background-color (color/->color (:id sender))}}]]
19 | :info [:span.bot-notice "BOT"]
20 | :name [:a.nickname {:tab-index -1
21 | :href sender-path}
22 | (:nickname sender)]})))
23 |
--------------------------------------------------------------------------------
/src/braid/bots/client/views/bots_page_styles.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.bots.client.views.bots-page-styles
2 | (:require
3 | [braid.core.client.ui.styles.mixins :as mixins]
4 | [braid.core.client.ui.styles.vars :as vars]
5 | [garden.units :refer [rem em px]]))
6 |
7 | (def bot-notice
8 | [:.thread>.card>.messages>.message>.info>.bot-notice
9 | {:background-color "#c0afc0"
10 | :border-radius (px 5)
11 | :padding (rem 0.25)
12 | :font-weight "bold"
13 | :color "#413f42"}])
14 |
15 | (def bots-page
16 | [:.app>.main>.page.bots
17 | [:>.title
18 | {:font-size "large"}]
19 | [:>.content
20 | (mixins/settings-container-style)
21 |
22 | [:>.bots
23 | (mixins/settings-item-style)
24 | [:>.bots-list
25 | mixins/flex
26 | {:flex-direction "row"
27 | :flex-wrap "wrap"
28 | :align-content "space-between"
29 | :align-items "baseline"}
30 |
31 | [:>.bot
32 | {:margin (em 1)}
33 | [:img {:margin "0 auto"}]
34 | [:button
35 | (mixins/outline-button {:text-color vars/grey-text
36 | :border-color "darkgray"
37 | :hover-border-color "lightgray"
38 | :hover-text-color "lightgray"})
39 | [:&.dangerous {:color "red"}]
40 | [:&.delete
41 | (mixins/fontawesome nil)]]
42 |
43 | [:>.avatar
44 | {:width (rem 4)
45 | :height (rem 4)
46 | :display "block"
47 | :border-radius (rem 1)
48 | :margin-bottom vars/pad}]]]]
49 |
50 | [:>.add-bot
51 | (mixins/settings-item-style)
52 | {:max-width "50%"
53 | :margin "0 auto"}
54 | [:form
55 | mixins/flex
56 | {:flex-direction "column"}
57 | [:.new-avatar>img
58 | {:max-width "150px"}]]]]])
59 |
--------------------------------------------------------------------------------
/src/braid/bots/schema.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.bots.schema
2 | (:require
3 | [clojure.spec.alpha :as s]
4 | [spec-tools.data-spec :as ds]))
5 |
6 | (def Bot
7 | {:id uuid?
8 | :group-id uuid?
9 | :user-id uuid?
10 | :name string?
11 | :avatar string?
12 | :webhook-url string?
13 | :event-webhook-url (ds/maybe string?)
14 | :token string?
15 | :notify-all-messages? boolean?})
16 |
17 | (def BotDisplay
18 | "Like Bot but for publicly-available bot info"
19 | {:id uuid?
20 | :user-id uuid?
21 | :nickname string?
22 | :avatar string?})
23 |
--------------------------------------------------------------------------------
/src/braid/bots/server.clj:
--------------------------------------------------------------------------------
1 | (ns braid.bots.server
2 | "Sending notification of messages and events to bots"
3 | (:require
4 | [braid.lib.crypto :as crypto]
5 | [braid.lib.transit :refer [->transit]]
6 | [org.httpkit.client :as http]
7 | [taoensso.timbre :as timbre])
8 | (:import
9 | (java.io ByteArrayInputStream)))
10 |
11 | (defn send-message-notification
12 | [bot message]
13 | (let [body (->transit message)
14 | hmac (crypto/hmac-bytes (bot :token) body)]
15 | (timbre/debugf "sending bot notification")
16 | (try
17 | ; TODO: should this be a POST too?
18 | (->>
19 | @(http/put (bot :webhook-url)
20 | {:headers {"Content-Type" "application/transit+msgpack"
21 | "X-Braid-Signature" hmac}
22 | :body (ByteArrayInputStream. body)})
23 | (timbre/debugf "Bot response: %s"))
24 | (catch Exception ex
25 | (timbre/warnf "Error sending bot notification: %s" ex)))))
26 |
27 | (defn send-event-notification
28 | [bot info]
29 | (when-let [url (:event-webhook-url bot)]
30 | (timbre/debugf "Sending event notification %s to %s" info bot)
31 | (let [body (->transit info)
32 | hmac (crypto/hmac-bytes (bot :token) body)]
33 | (timbre/debugf "sending bot event notification")
34 | (try
35 | (->>
36 | @(http/post url
37 | {:headers {"Content-Type" "application/transit+msgpack"
38 | "X-Braid-Signature" hmac}
39 | :body (ByteArrayInputStream. body)})
40 | (timbre/debugf "Bot response: %s"))
41 | (catch Exception ex
42 | (timbre/warnf "Error sending bot event notification: %s" ex))))))
43 |
--------------------------------------------------------------------------------
/src/braid/bots/util.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.bots.util)
2 |
3 | (def bot-name-re
4 | #"(?:\w|\d){1,30}")
5 |
--------------------------------------------------------------------------------
/src/braid/chat/db/invitation.clj:
--------------------------------------------------------------------------------
1 | (ns braid.chat.db.invitation
2 | (:require
3 | [braid.core.server.db :as db]
4 | [braid.chat.db.common :refer [create-entity-txn db->invitation]]
5 | [datomic.api :as d]))
6 |
7 | ;; Queries
8 |
9 | (defn invite-by-id
10 | [invite-id]
11 | (some-> (d/pull (db/db)
12 | [:invite/id
13 | {:invite/from [:user/id :user/email :user/nickname]}
14 | :invite/to
15 | {:invite/group [:group/id :group/name]}]
16 | [:invite/id invite-id])
17 | db->invitation))
18 |
19 | (defn invites-for-user
20 | [user-id]
21 | (->> (d/q '[:find (pull ?i [{:invite/group [:group/id :group/name]}
22 | {:invite/from [:user/id :user/email :user/nickname]}
23 | :invite/to
24 | :invite/id])
25 | :in $ ?user-id
26 | :where
27 | [?u :user/id ?user-id]
28 | [?u :user/email ?email]
29 | [?i :invite/to ?email]]
30 | (db/db) user-id)
31 | (map (comp db->invitation first))))
32 |
33 | ;; Transactions
34 |
35 | (defn create-invitation-txn
36 | [{:keys [id inviter-id invitee-email group-id]}]
37 | (create-entity-txn
38 | {:invite/id id
39 | :invite/group [:group/id group-id]
40 | :invite/from [:user/id inviter-id]
41 | :invite/to invitee-email
42 | :invite/created-at (java.util.Date.)}
43 | db->invitation))
44 |
45 | (defn retract-invitation-txn
46 | [invite-id]
47 | [[:db.fn/retractEntity [:invite/id invite-id]]])
48 |
--------------------------------------------------------------------------------
/src/braid/chat/events.clj:
--------------------------------------------------------------------------------
1 | (ns braid.chat.events
2 | (:require
3 | [braid.core.server.db :as db]
4 | [braid.chat.db.group :as group]
5 | [braid.chat.db.thread :as thread]
6 | [braid.chat.db.user :as user]
7 | [braid.core.server.sync-helpers :as sync-helpers]))
8 |
9 | (defn user-join-group!
10 | [user-id group-id]
11 | (db/run-txns! (group/user-join-group-txn user-id group-id))
12 | (db/run-txns! (group/user-open-recent-threads user-id group-id))
13 | (sync-helpers/broadcast-group-change
14 | group-id
15 | [:braid.client/new-user [(-> (user/user-by-id user-id)
16 | (assoc :joined-at (java.util.Date.)))
17 | group-id]]))
18 |
19 | (defn register-user!
20 | [{:keys [email password group-id]}]
21 | (let [[user] (db/run-txns! (user/create-user-txn {:id (db/uuid)
22 | :email email}))]
23 | (db/run-txns! (user/set-user-password-txn (:id user) password))
24 | (user-join-group! (user :id) group-id)
25 | (user :id)))
26 |
--------------------------------------------------------------------------------
/src/braid/chat/server/initial_data.clj:
--------------------------------------------------------------------------------
1 | (ns braid.chat.server.initial-data
2 | (:require
3 | [braid.base.conf :as conf]
4 | [braid.base.server.socket :refer [connected-uids]]
5 | [braid.chat.db.group :as group]
6 | [braid.chat.db.invitation :as invitation]
7 | [braid.chat.db.tag :as tag]
8 | [braid.chat.db.thread :as thread]
9 | [braid.chat.db.user :as user]
10 | [braid.lib.digest :as digest]))
11 |
12 | (defn initial-data-for-user
13 | [user-id]
14 | (let [connected (set (:any @connected-uids))
15 | user-status (fn [user] (if (connected (user :id)) :online :offline))
16 | update-user-statuses (fn [users]
17 | (reduce-kv
18 | (fn [m id u]
19 | (assoc m id (assoc u :status (user-status u))))
20 | {} users))]
21 | {;; TODO could be part of braid.base
22 | :user-id user-id
23 | ;; TODO could be part of braid.base:
24 | :version-checksum (if (boolean (conf/config :prod-js))
25 | (digest/from-file "public/js/prod/desktop.js")
26 | (digest/from-file "public/js/dev/desktop.js"))
27 | :user-groups
28 | (->> (group/user-groups user-id)
29 | (map (fn [group] (update group :users update-user-statuses)))
30 | (map (fn [group]
31 | (->> (group/group-users-joined-at (:id group))
32 | (reduce (fn [group [user-id joined-at]]
33 | (assoc-in
34 | group
35 | [:users user-id :joined-at]
36 | joined-at))
37 | group)))))
38 | :user-threads (thread/open-threads-for-user user-id)
39 | :user-subscribed-tag-ids (tag/subscribed-tag-ids-for-user user-id)
40 | :user-preferences (user/user-get-preferences user-id)
41 | :invitations (invitation/invites-for-user user-id)
42 | :tags (tag/tags-for-user user-id)}))
43 |
--------------------------------------------------------------------------------
/src/braid/core.clj:
--------------------------------------------------------------------------------
1 | (ns braid.core
2 | (:gen-class)
3 | (:require
4 | [environ.core :refer [env]]
5 | [mount.core :as mount]
6 | [org.httpkit.client]
7 | [org.httpkit.sni-client]
8 | [taoensso.timbre :as timbre]
9 | [braid.base.conf]
10 | [braid.core.modules :as modules]
11 | ;; all following requires are for mount:
12 | [braid.core.server.core]
13 | [braid.core.server.email-digest]))
14 |
15 | ;; because we often use http-kit as our http-client
16 | ;; including this so that SNI works
17 | (alter-var-root #'org.httpkit.client/*default-client*
18 | (fn [_] org.httpkit.sni-client/default-client))
19 |
20 | (defn start!
21 | "Entry point for prod"
22 | ([port]
23 | (start! port
24 | (cond-> env
25 | (env :aws-access-key)
26 | (assoc :aws/credentials-provider
27 | (constantly ((juxt :aws-access-key :aws-secret-key) env))))))
28 | ([port args]
29 | ;; modules must run first
30 | (when (empty? (args :hmac-secret))
31 | (timbre/warn "No :hmac-secret set, using an insecure default."))
32 | (modules/init! modules/default)
33 | (-> (mount/with-args (assoc args :port port))
34 | (mount/start))
35 | (when (zero? port)
36 | (mount/stop #'braid.base.conf/config)
37 | (mount/start #'braid.base.conf/config))))
38 |
39 | (defn stop!
40 | "Helper function for stopping all Braid components."
41 | []
42 | (mount/stop))
43 |
44 | (defn -main
45 | "Entry point for prod"
46 | [& args]
47 | (let [port (Integer/parseInt (first args))]
48 | (start! port)))
49 |
--------------------------------------------------------------------------------
/src/braid/core/client/desktop/core.cljs:
--------------------------------------------------------------------------------
1 | (ns ^:figwheel-hooks
2 | braid.core.client.desktop.core
3 | (:require
4 | [braid.base.client.events]
5 | [braid.base.client.subs]
6 | [braid.chat.client.events]
7 | [braid.chat.client.subs]
8 | [braid.core.client.gateway.events]
9 | [braid.core.client.gateway.subs]
10 | [braid.core.client.group-admin.events]
11 | [braid.core.client.group-admin.subs]
12 | [braid.core.client.invites.events]
13 | [braid.core.client.invites.subs]
14 | [braid.base.client.router :as router]
15 | [braid.base.client.remote-handlers]
16 | [braid.core.client.ui.views.app :refer [app-view]]
17 | [braid.core.modules :as modules]
18 | [re-frame.core :as rf :refer [dispatch-sync dispatch]]
19 | [reagent.dom :as r-dom]))
20 |
21 | (enable-console-print!)
22 |
23 | (defn render []
24 | (r-dom/render [app-view] (.getElementById js/document "app")))
25 |
26 | (defn ^:export init
27 | ([] (init modules/default))
28 | ([modules]
29 | (modules/init! modules)
30 |
31 | (dispatch-sync [:initialize-db!])
32 |
33 | (.addEventListener js/document "visibilitychange"
34 | (fn [e]
35 | (dispatch [:set-window-visibility!
36 | (= "visible" (.-visibilityState js/document))])))
37 |
38 | (render)
39 |
40 | (router/init)))
41 |
42 | (defn ^:after-load reload
43 | "Force a re-render. For use with figwheel"
44 | []
45 | (modules/init! modules/default)
46 | (render))
47 |
--------------------------------------------------------------------------------
/src/braid/core/client/desktop/notify.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.desktop.notify
2 | (:require
3 | [goog.object :as o]))
4 |
5 | (defn has-notify?
6 | []
7 | (some? (o/get js/window "Notification")))
8 |
9 | (defn enabled?
10 | []
11 | (= "granted" (.-permission js/Notification)))
12 |
13 | (defn request-permission
14 | [cb]
15 | (.requestPermission js/Notification (fn [perm] (cb perm))))
16 |
17 | (defn notify
18 | [{:keys [title body icon] :or {title "Braid"
19 | icon "https://braid.chat/images/braid.svg"}}]
20 | (js/Notification. title #js {:body body :icon icon}))
21 |
--------------------------------------------------------------------------------
/src/braid/core/client/gateway/core.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.gateway.core
2 | (:require
3 | [braid.core.client.gateway.events :as events]
4 | [braid.core.client.gateway.subs]
5 | [braid.core.client.gateway.views :refer [gateway-view]]
6 | [braid.core.modules :as modules]
7 | [goog.object :as o]
8 | [re-frame.core :refer [dispatch-sync]]
9 | [reagent.dom :as r-dom]))
10 |
11 | (enable-console-print!)
12 |
13 | (defn render []
14 | (r-dom/render [gateway-view] (. js/document (getElementById "app"))))
15 |
16 | (defn ^:export init []
17 | (modules/init! modules/default)
18 | (dispatch-sync [::events/initialize! (keyword (o/get js/window "gateway_action"))])
19 | (render))
20 |
21 | (defn ^:export reload []
22 | (render))
23 |
--------------------------------------------------------------------------------
/src/braid/core/client/gateway/events.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.gateway.events
2 | (:require
3 | [braid.core.client.gateway.forms.join-group.events :as join-group]
4 | [braid.core.client.gateway.forms.user-auth.events :as user-auth]
5 | [braid.core.client.gateway.fx]
6 | [braid.base.client.state :refer [reg-event-fx]] ;; TODO should use base.api/register-events!
7 | [re-frame.core :refer [dispatch]]))
8 |
9 | (reg-event-fx
10 | ::initialize!
11 | (fn [{state :db} [_ action]]
12 | {:db (-> state
13 | (assoc :action action)
14 | (assoc :action-disabled? true))
15 | :dispatch [::handle-action!]}))
16 |
17 | (reg-event-fx
18 | ::handle-action!
19 | (fn [{state :db} _]
20 | (case (state :action)
21 | :create-group
22 | {:dispatch [:braid.group-create.core/initialize!]}
23 |
24 | :log-in
25 | {:dispatch [::user-auth/initialize! :log-in]}
26 |
27 | :request-password-reset
28 | {:dispatch [::user-auth/initialize! :request-password-reset]}
29 |
30 | :join-group
31 | {:dispatch [::join-group/initialize!]})))
32 |
33 | (reg-event-fx
34 | ::change-user-status!
35 | (fn [{state :db} [_ logged-in?]]
36 | (merge
37 | {:db (assoc state :action-disabled? (not logged-in?))}
38 | (cond
39 | (and (= :log-in (state :action)) logged-in?)
40 | {:dispatch [:start-socket!]}
41 |
42 | (and (= :join-group (state :action)) logged-in?)
43 | {:dispatch [::join-group/remote-join-group!]}
44 |
45 | :else {}))))
46 |
--------------------------------------------------------------------------------
/src/braid/core/client/gateway/forms/join_group/events.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.gateway.forms.join-group.events
2 | (:require
3 | [braid.core.client.gateway.helpers :as helpers :refer [get-url-group-id]]
4 | [braid.base.client.state :refer [reg-event-fx]] ;; TODO should use reg.base.api/register-events!
5 | [clojure.string :as string]
6 | [re-frame.core :refer [dispatch]]))
7 |
8 | (reg-event-fx
9 | ::initialize!
10 | (fn [{state :db}]
11 | {:dispatch-n [[:braid.core.client.gateway.forms.user-auth.events/initialize! :log-in]]}))
12 |
13 | (reg-event-fx
14 | ::remote-join-group!
15 | (fn [{state :db} _]
16 | {:edn-xhr {:method :put
17 | :uri (str "/groups/" (get-url-group-id) "/join")
18 | :headers {"x-csrf-token" (state :csrf-token)}
19 | :on-complete
20 | (fn [response]
21 | (set! js/window.location (str "/groups/" (get-url-group-id) "/inbox")))
22 | :on-error
23 | (fn [error]
24 | (when-let [k (get-in error [:response :error])]
25 | (dispatch [:braid.notices/display! [(keyword "join-group-error" (get-url-group-id)) k :error]])))}}))
26 |
--------------------------------------------------------------------------------
/src/braid/core/client/gateway/forms/user_auth/subs.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.gateway.forms.user-auth.subs
2 | (:require
3 | [braid.core.client.gateway.helpers :as helpers]
4 | [re-frame.core :refer [reg-sub]]))
5 |
6 | (reg-sub
7 | ::user
8 | (fn [state _]
9 | (get-in state [:user-auth :user])))
10 |
11 | (reg-sub
12 | ::user-auth-mode
13 | (fn [state _]
14 | (cond
15 | (nil? (get-in state [:user-auth])) :checking
16 | (get-in state [:user-auth :checking?]) :checking
17 | (get-in state [:user-auth :user]) :authed
18 | (get-in state [:user-auth :oauth-provider]) :oauth-in-progress
19 | :else (get-in state [:user-auth :mode]))))
20 |
21 | (reg-sub
22 | ::oauth-provider
23 | (fn [state _]
24 | (get-in state [:user-auth :oauth-provider])))
25 |
26 | (reg-sub
27 | ::error
28 | (fn [state _]
29 | (get-in state [:user-auth :error])))
30 |
31 | (helpers/reg-form-subs :braid.core.client.gateway.forms.user-auth.subs :user-auth)
32 |
--------------------------------------------------------------------------------
/src/braid/core/client/gateway/forms/user_auth/validations.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.gateway.forms.user-auth.validations
2 | (:require
3 | [clojure.string :as string])
4 | (:import
5 | (goog.format EmailAddress)))
6 |
7 | (def validations
8 | {:email
9 | [(fn [email cb]
10 | (if (string/blank? email)
11 | (cb "You need to enter an email.")
12 | (cb nil)))
13 | (fn [email cb]
14 | (if (not (.isValid (EmailAddress. email)))
15 | (cb "This doesn't look like a valid email.")
16 | (cb nil)))]
17 |
18 | :password
19 | [(fn [password cb]
20 | (if (string/blank? password)
21 | (cb "You need to enter a password.")
22 | (cb nil)))]
23 |
24 | :new-password
25 | [(fn [password cb]
26 | (if (string/blank? password)
27 | (cb "You need to enter a password.")
28 | (cb nil)))
29 | (fn [password cb]
30 | (if (< (count password) 8)
31 | (cb "Your password is too short.")
32 | (cb nil)))]})
33 |
--------------------------------------------------------------------------------
/src/braid/core/client/gateway/fx.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.gateway.fx
2 | (:require
3 | [braid.lib.xhr :as xhr]
4 | [braid.core.client.state.fx.dispatch-debounce :as fx.debounce]
5 | [re-frame.core :refer [dispatch reg-fx]]))
6 |
7 | (reg-fx :dispatch-debounce
8 | fx.debounce/dispatch)
9 |
10 | (reg-fx :stop-debounce
11 | fx.debounce/stop)
12 |
13 | (reg-fx :edn-xhr xhr/edn-xhr)
14 |
--------------------------------------------------------------------------------
/src/braid/core/client/gateway/styles_vars.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.gateway.styles-vars
2 | (:require
3 | [braid.core.client.ui.styles.fontawesome :as fontawesome]))
4 |
5 | (def small-spacing "0.5rem")
6 | (def border-radius "3px")
7 | (def font-family "Open Sans")
8 |
9 | (def accent-color "#2bb8ba")
10 | (def page-background-color "#f3f3f3")
11 | (def form-background-color "#ffffff")
12 | (def primary-text-color "#444444")
13 | (def secondary-text-color "#999999")
14 | (def field-border-color "#e0e0e0")
15 | (def invalid-color "#fd4734")
16 | (def invalid-light-color "#ff8477")
17 | (def valid-color "#2bb8ba")
18 | (def disabled-input-color "#f6f6f6")
19 | (def placeholder-color "#eeeeee")
20 | (def disabled-button-color "#cccccc")
21 |
22 | (defn input-field-mixin []
23 | {:font-size "1.25rem"
24 | :font-family font-family
25 | :padding "0 0.4em"
26 | :width "100%"
27 | :box-sizing "border-box"
28 | :border [["1px" "solid" field-border-color]]
29 | :line-height "2em"
30 | :height "2em"
31 | :border-radius border-radius})
32 |
33 | (defn small-text-mixin []
34 | [:&
35 | {:color secondary-text-color
36 | :font-size "0.75em"
37 | :margin [[small-spacing 0 0 0]]}
38 | [:p
39 | {:margin "0"}]])
40 |
41 | (defn small-button-mixin []
42 | [:&
43 | {:display "inline-block"
44 | :border-radius border-radius
45 | :padding [[0 "0.5em"]]
46 | :margin-left "0.5em"
47 | :background "none"
48 | :color secondary-text-color
49 | :cursor "pointer"
50 | :font-size "0.8em"
51 | :height "1.25rem"
52 | :line-height "1.25rem"
53 | :letter-spacing "0.02em"
54 | :text-transform "uppercase"
55 | :box-sizing "border-box"
56 | :border [["1px" "solid" field-border-color]]
57 | :transition ["border-color 0.25s ease-in-out"
58 | "color 0.25s ease-in-out"]}
59 |
60 | [:&::after
61 | (fontawesome/mixin :angle-double-right)
62 | {:margin-left "0.25em"}]
63 |
64 | [:&:hover
65 | {:color "#666666"
66 | :border-color "#999999"}]
67 |
68 | [:&:active
69 | {:background secondary-text-color}]])
70 |
71 | (defn padded-with-border-mixin []
72 | {:padding "1em"
73 | :border [["1px" "solid" field-border-color]]
74 | :border-radius border-radius})
75 |
--------------------------------------------------------------------------------
/src/braid/core/client/gateway/subs.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.gateway.subs
2 | (:require
3 | [re-frame.core :refer [reg-sub]]
4 | [braid.core.client.gateway.forms.user-auth.subs]))
5 |
6 | (reg-sub
7 | ::action
8 | (fn [state _]
9 | (get-in state [:action])))
10 |
11 | (reg-sub
12 | ::action-disabled?
13 | (fn [state _]
14 | (get-in state [:action-disabled?])))
15 |
--------------------------------------------------------------------------------
/src/braid/core/client/gateway/views.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.gateway.views
2 | (:require
3 | [braid.core.client.gateway.forms.user-auth.styles :refer [user-auth-styles]]
4 | [braid.core.client.gateway.forms.user-auth.views :refer [user-auth-view]]
5 | [braid.core.client.gateway.styles :as styles]
6 | [braid.core.client.ui.styles.imports :refer [imports]]
7 | [garden.core :refer [css]]
8 | [re-frame.core :refer [subscribe]]))
9 |
10 | (defn style-view []
11 | [:style
12 | {:type "text/css"
13 | :dangerouslySetInnerHTML
14 | {:__html
15 | (css {:auto-prefix #{:transition
16 | :flex-direction
17 | :flex-shrink
18 | :align-items
19 | :animation
20 | :flex-grow}
21 | :vendors ["webkit"]}
22 | imports
23 | styles/anim-spin
24 | (styles/app-styles)
25 | [:#app
26 | (styles/form-styles)
27 | (user-auth-styles)])}}])
28 |
29 | (defn header-view []
30 | [:h1.header "Braid"])
31 |
32 | (defn gateway-view []
33 | [:div.gateway
34 | [style-view]
35 | [header-view]
36 | [user-auth-view]])
37 |
--------------------------------------------------------------------------------
/src/braid/core/client/group_admin/events.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.group-admin.events
2 | (:require
3 | [braid.base.client.state :refer [reg-event-fx]] ;; TODO should use base.apie/register-events!
4 | ))
5 |
6 | (reg-event-fx
7 | :set-group-intro!
8 | (fn [{state :db} [_ {:keys [group-id intro local-only?] :as args}]]
9 | {:db (assoc-in state [:groups group-id :intro] intro)
10 | :websocket-send (when-not local-only?
11 | (list [:braid.server/set-group-intro args]))}))
12 |
13 | (reg-event-fx
14 | :set-group-avatar!
15 | (fn [{state :db} [_ {:keys [group-id avatar local-only?] :as args}]]
16 | {:db (assoc-in state [:groups group-id :avatar] avatar)
17 | :websocket-send (when-not local-only?
18 | (list [:braid.server/set-group-avatar args]))}))
19 |
20 | (reg-event-fx
21 | :make-group-public!
22 | (fn [_ [_ group-id]]
23 | {:websocket-send (list [:braid.server/set-group-publicity [group-id true]])}))
24 |
25 | (reg-event-fx
26 | :make-group-private!
27 | (fn [_ [_ group-id]]
28 | {:websocket-send (list [:braid.server/set-group-publicity [group-id false]])}))
29 |
30 | (reg-event-fx
31 | :set-group-publicity!
32 | (fn [{db :db} [_ [group-id publicity]]]
33 | {:db (assoc-in db [:groups group-id :public?] publicity)}))
34 |
--------------------------------------------------------------------------------
/src/braid/core/client/group_admin/subs.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.group-admin.subs)
2 |
--------------------------------------------------------------------------------
/src/braid/core/client/group_admin/views/group_settings_page_styles.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.group-admin.views.group-settings-page-styles
2 | (:require
3 | [braid.core.client.ui.styles.mixins :as mixins]
4 | [braid.core.client.ui.styles.vars :as vars]
5 | [garden.units :refer [rem em px]]))
6 |
7 | (def group-settings-page
8 | [:>.page.group-settings
9 | (mixins/settings-style)])
10 |
--------------------------------------------------------------------------------
/src/braid/core/client/helpers.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.helpers
2 | (:require-macros
3 | [cljs.core.async.macros :refer [go]])
4 | (:require
5 | [cljs.core.async :refer [.page.invite
9 | [:>.content
10 | (mixins/settings-container-style)
11 | [:>.invite
12 | (mixins/settings-item-style)]]])
13 |
--------------------------------------------------------------------------------
/src/braid/core/client/routes.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.routes
2 | (:require
3 | [braid.base.client.router :as router]
4 | [braid.base.client.pages :as pages]
5 | [re-frame.core :refer [dispatch subscribe]]
6 | [secretary.core :as secretary :refer-macros [defroute]]))
7 |
8 | (defn go-to! [path]
9 | (router/go-to path))
10 |
11 | (defroute join-group-path "/groups/:group-id/join" [group-id]
12 | (dispatch [:braid.core.client.gateway.events/initialize! :join-group])
13 | (dispatch [:set-group-and-page! [nil {:type :login}]]))
14 |
15 | (defroute group-page-path "/groups/:group-id/:page-id" [group-id page-id query-params]
16 | (let [page (merge {:type (keyword page-id)
17 | :page-id (keyword page-id)
18 | :group-id (uuid group-id)}
19 | query-params)]
20 | (pages/on-exit! (:page-id @(subscribe [:page])) @(subscribe [:page]))
21 | (dispatch [:set-group-and-page! [(uuid group-id) page]])
22 | (pages/on-load! (keyword page-id) page)))
23 |
24 | ;; invites end up here
25 | (defroute group-path "/groups/:group-id" [group-id]
26 | (router/go-to (group-page-path {:group-id group-id
27 | :page-id "inbox"})))
28 |
29 | (defroute system-page-path "/pages/:page-id" [page-id query-params]
30 | (let [page (merge {:type (keyword page-id)
31 | :page-id (keyword page-id)}
32 | query-params)]
33 | (pages/on-exit! (:page-id @(subscribe [:page])) @(subscribe [:page]))
34 | (dispatch [:set-group-and-page! [nil page]])
35 | (pages/on-load! (keyword page-id) page)))
36 |
37 | (defroute index-path "/" {}
38 | (dispatch [:set-group-and-page! [nil {:type :login}]])
39 | (dispatch [:redirect-from-root!]))
40 |
--------------------------------------------------------------------------------
/src/braid/core/client/schema.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.schema
2 | (:require
3 | [cljs-uuid-utils.core :as uuid]))
4 |
5 | (defn make-message [data]
6 | {:id (or (data :id) (uuid/make-random-squuid))
7 | :content (data :content)
8 | :thread-id (or (data :thread-id) (uuid/make-random-squuid))
9 | :group-id (data :group-id)
10 | :user-id (data :user-id)
11 | :created-at (or (data :created-at) (js/Date.))
12 | :mentioned-user-ids (or (data :mentioned-user-ids) [])
13 | :mentioned-tag-ids (or (data :mentioned-tag-ids) [])})
14 |
15 | (defn make-tag []
16 | {:id (uuid/make-random-squuid)
17 | :name nil
18 | :group-id nil
19 | :description nil
20 | :threads-count 0
21 | :subscribers-count 0})
22 |
23 | (defn make-group [data]
24 | {:id (or (data :id) (uuid/make-random-squuid))
25 | :name (data :name)
26 | :slug (data :slug)
27 | :admins #{}
28 | :intro nil
29 | :avatar nil
30 | :public? false
31 | :bots #{}})
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/braid/core/client/state/fx/dispatch_debounce.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.state.fx.dispatch-debounce
2 | (:require
3 | [re-frame.core :as re-frame]))
4 |
5 | (defonce timeouts
6 | (atom {}))
7 |
8 | (defn dispatch
9 | [[id event-vec n]]
10 | (js/clearTimeout (@timeouts id))
11 | (swap! timeouts assoc id
12 | (js/setTimeout (fn []
13 | (re-frame/dispatch event-vec)
14 | (swap! timeouts dissoc id))
15 | n)))
16 |
17 | (defn stop [id]
18 | (js/clearTimeout (@timeouts id))
19 | (swap! timeouts dissoc id))
20 |
--------------------------------------------------------------------------------
/src/braid/core/client/state/fx/redirect.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.state.fx.redirect
2 | (:require
3 | [braid.base.client.router :as router]))
4 |
5 | (defn redirect-to [route]
6 | (when route (router/go-to route)))
7 |
--------------------------------------------------------------------------------
/src/braid/core/client/state/subscription.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.state.subscription)
2 |
3 | (defmulti subscription
4 | "Create a reaction for the particular type of information.
5 | Do not call directly, should be invoked by `subscribe`"
6 | {:arglists '([state [sub-name args]])}
7 | (fn [_ [sub-name _]] sub-name))
8 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/styles/animations.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.styles.animations
2 | (:require
3 | [garden.stylesheet :refer [at-keyframes]]))
4 |
5 | (def anim-spin
6 | (at-keyframes :anim-spin
7 | ["0%" {:transform "rotate(0deg)"}]
8 | ["100%" {:transform "rotate(359deg)"}]))
9 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/styles/body.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.styles.body
2 | (:require
3 | [braid.core.client.ui.styles.mixins :as mixins]))
4 |
5 | (def body
6 | [:body
7 | mixins/standard-font
8 | {:margin 0
9 | :padding 0
10 | :background "#F2F2F2"}
11 | ; prevent overscroll:
12 | {:height "100%"
13 | :overflow "hidden"}
14 |
15 | [:textarea :input
16 | {:font-family "inherit"
17 | :font-size "1em"}]])
18 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/styles/fontawesome.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.styles.fontawesome
2 | (:require
3 | [braid.core.client.ui.styles.mixins :as mixins]))
4 |
5 | (def icons
6 | {:github \uf09b
7 | :facebook \uf09a
8 | :google \uf1a0
9 | :caret-right \uf0da
10 | :chevron-right \uf054
11 | :angle-right \uf105
12 | :angle-double-right \uf101
13 | :warning \uf071
14 | :check-circle \uf00c
15 | :spinner \uf110
16 | :users \uf0c0})
17 |
18 | (defn mixin [code]
19 | (mixins/fontawesome (or (icons code) code)))
20 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/styles/hljs.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.styles.hljs)
2 |
3 | (def foreground "#eaeaea")
4 | (def background "#000000")
5 | (def selection "#424242")
6 | (def line "#2a2a2a")
7 | (def comment-color "#969896")
8 | (def red "#d54e53")
9 | (def orange "#e78c45")
10 | (def yellow "#e7c547")
11 | (def green "#b9ca4a")
12 | (def aqua "#70c0b1")
13 | (def blue "#7aa6da")
14 | (def purple "#c397d8")
15 | (def window "#4d5057")
16 |
17 | (def hljs-styles
18 | [:>.hljs
19 |
20 | [:.hljs-comment
21 | :.hljs-quote
22 | {:color comment-color}]
23 |
24 | [:.hljs-variable
25 | :.hljs-template-variable
26 | :.hljs-tag
27 | :.hljs-name
28 | :.hljs-selector-id
29 | :.hljs-selector-class
30 | :.hljs-regexp
31 | :.hljs-deletion
32 | {:color red}]
33 |
34 | [:.hljs-number
35 | :.hljs-built_in
36 | :.hljs-builtin-name
37 | :.hljs-literal
38 | :.hljs-type
39 | :.hljs-params
40 | :.hljs-meta
41 | :.hljs-link
42 | {:color orange}]
43 |
44 | [:.hljs-attribute
45 | {:color yellow}]
46 |
47 | [:.hljs-symbol
48 | {:color aqua}]
49 |
50 | [:.hljs-string
51 | :.hljs-bullet
52 | :.hljs-addition
53 | {:color green}]
54 |
55 | [:.hljs-title
56 | :.hljs-section
57 | {:color blue}]
58 |
59 | [:.hljs-keyword
60 | :.hljs-selector-tag
61 | {:color purple}]
62 |
63 | [:.hljs-emphasis
64 | {:font-style "italic"}]
65 |
66 | [:.hljs-strong
67 | {:font-weight "bold"}]])
68 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/styles/hover_menu.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.styles.hover-menu
2 | (:require
3 | [garden.arithmetic :as m]
4 | [garden.units :refer [px]]
5 | [braid.core.client.ui.styles.mixins :as mixins]
6 | [braid.core.client.ui.styles.vars :as vars]))
7 |
8 | (defn >hover-menu []
9 | [:>.hover-menu
10 | {:background "white"
11 | :border "0.5px solid #ddd"
12 | :border-radius vars/border-radius
13 | ;; use filter instead of box-shadow
14 | ;; so that it takes the shape of the arrow too
15 | :filter "drop-shadow(0 1px 2px #ccc)"}
16 |
17 | [:&.top
18 | {:position "absolute"
19 | :bottom "100%"}]
20 |
21 | [:&.bottom
22 | {:position "absolute"
23 | :top "100%"}]
24 |
25 | [:&.left
26 | {:position "absolute"
27 | :right "100%"}]
28 |
29 | [:&.right
30 | {:position "absolute"
31 | :left "100%"}]
32 |
33 | ;; arrow
34 | (let [size (px 10)]
35 | [:>.arrow
36 | {:position "absolute"
37 | :width size
38 | :height size
39 | :fill "white"
40 | :stroke "#ddd"
41 | :stroke-width "0.51"}
42 |
43 | [:&.top
44 | {:bottom (m/- size)
45 | :left "11px"}]
46 |
47 | [:&.bottom
48 | {:top (m/- size)
49 | :left "11px"}]
50 |
51 | [:&.left
52 | {:right (m/- size)
53 | :top "11px"}]
54 |
55 | [:&.right
56 | {:left (m/- size)
57 | :top "11px"}]])
58 |
59 | [:.content
60 | {:overflow-x "auto"
61 | :height "100%"
62 | :box-sizing "border-box"
63 | :padding [[(m/* vars/pad 0.75)]]}
64 |
65 | [:>a
66 | {:display "block"
67 | :color "black"
68 | :text-align "right"
69 | :text-decoration "none"
70 | :line-height "1.85em"
71 | :white-space "nowrap"
72 | :cursor "pointer"}
73 |
74 | [:&:hover
75 | {:color "#666"}]
76 |
77 | [:>.icon
78 | {:display "inline-block"
79 | :margin-left "0.5em"
80 | :width "1.5em"}
81 | (mixins/fontawesome nil)]]]])
82 |
83 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/styles/imports.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.styles.imports
2 | (:require
3 | [garden.stylesheet :refer [at-import at-font-face]]))
4 |
5 | (def fa-at-font-face
6 | (at-font-face
7 | {:font-family "\"Font Awesome 5 Free\""
8 | :src ["url('/fonts/fa-solid-900.eot?#iefix') format('embedded-opentype')"
9 | "url('/fonts/fa-solid-900.woff2') format('woff2')"
10 | "url('/fonts/fa-solid-900.woff') format('woff')"
11 | "url('/fonts/fa-solid-900.ttf') format('truetype')"
12 | "url('/fonts/fa-solid-900.svg#fontawesome') format('svg')"]
13 | :font-weight 900
14 | :font-display "block"
15 | :font-style "normal"}))
16 |
17 | (def fab-at-font-face
18 | (at-font-face
19 | {:font-family "\"Font Awesome 5 Brands\""
20 | :src ["url('/fonts/fa-brands-400.eot?#iefix') format('embedded-opentype')"
21 | "url('/fonts/fa-brands-400.woff2') format('woff2')"
22 | "url('/fonts/fa-brands-400.woff') format('woff')"
23 | "url('/fonts/fa-brands-400.ttf') format('truetype')"
24 | "url('/fonts/fa-brands-400.svg#fontawesome') format('svg')"]
25 | :font-weight 400
26 | :font-display "block"
27 | :font-style "normal"}))
28 |
29 | (def imports
30 | [fa-at-font-face
31 | fab-at-font-face
32 | (at-import "https://fonts.googleapis.com/css?family=Open+Sans:400,300,400italic,700")])
33 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/styles/misc.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.styles.misc
2 | (:require
3 | [braid.core.client.ui.styles.mixins :as mixins]
4 | [braid.core.client.ui.styles.vars :as vars]
5 | [garden.arithmetic :as m]
6 | [garden.units :refer [rem em px ex]]))
7 |
8 | (def page-headers
9 | [[:.page>.title::before
10 | {:margin-right (em 0.5)}]
11 |
12 | [:.page.users>.title::before
13 | (mixins/fontawesome \uf0c0)]
14 |
15 | [:.page.settings>.title::before
16 | (mixins/fontawesome \uf013)]
17 |
18 | [:.page.search>.title::before
19 | (mixins/fontawesome \uf002)]
20 |
21 | [:.page.me>.title::before
22 | (mixins/fontawesome \uf007)]
23 |
24 | [:.page.userpage>.title::before
25 | (mixins/fontawesome \uf007)]])
26 |
27 | (def status
28 | [:>.status
29 | mixins/flex
30 | {:position "absolute"
31 | :top 0
32 | :bottom 0
33 | :right 0
34 | :left 0
35 | :background vars/page-background-color
36 | :color vars/primary-text-color
37 | :justify-content "center"
38 | :align-items "center"
39 | :font-size "2em"}
40 |
41 | [:&::before
42 | (mixins/fontawesome \uf110)
43 | mixins/spin
44 | {:display "inline-block"
45 | :margin-right (em 0.5)}]])
46 |
47 | (defn drag-and-drop [pad]
48 | [:&
49 |
50 | [:&.dragging
51 | {:background-color "lightgray"
52 | :border [[(px 5) "dashed" "darkgray"]]}]
53 |
54 | [:>.uploading-indicator
55 | (mixins/fontawesome \uf110)
56 | mixins/spin
57 | {:font-size (em 1.5)
58 | :text-align "center"}]])
59 |
60 | (defn avatar-upload [pad]
61 | [:&
62 | (drag-and-drop pad)])
63 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/styles/page.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.styles.page
2 | (:require
3 | [braid.core.client.ui.styles.vars :as vars]
4 | [garden.units :refer [rem]]))
5 |
6 | (def page
7 | [:>.page
8 | {:position "absolute"
9 | :left vars/sidebar-width
10 | :top vars/pad
11 | :bottom 0
12 | :right 0
13 | :margin-top vars/top-bar-height
14 | :overflow-x "auto"}
15 |
16 | [:>.title
17 | {:height vars/top-bar-height
18 | :line-height vars/top-bar-height
19 | :color vars/grey-text
20 | :margin [[vars/pad 0 0 vars/pad]]}]
21 |
22 | [:>.intro
23 | {:color vars/grey-text
24 | :margin vars/pad
25 | ; make intro above threads, so you can click on clear inbox button
26 | :z-index 100
27 | :position "relative"}]
28 |
29 | [:>.content
30 | {:padding vars/pad
31 | :color vars/darkgrey-text}
32 |
33 | [:>.description
34 | {:width vars/card-width
35 | ; we want description to appear above the new thread, but below other threads
36 | ; TODO: is there a way to make description clickable but not cover
37 | ; threads that are long enough?
38 | :z-index 100
39 | ; opacity & position are hacks to make z-index work
40 | :position "relative" ; z-index only applies if element has a position set
41 | :opacity 0.99 ; need to create a new rendering context b/c threads are absolute postioned
42 | }]]])
43 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/styles/pages/global_settings.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.styles.pages.global-settings
2 | (:require
3 | [braid.core.client.ui.styles.mixins :as mixins]
4 | [garden.units :refer [rem px]]))
5 |
6 | (def global-settings-page
7 | [:>.page.global-settings
8 | [:>.title {:font-size "large"}]
9 | (mixins/settings-style)])
10 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/styles/pages/me.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.styles.pages.me
2 | (:require
3 | [braid.core.client.ui.styles.mixins :as mixins]
4 | [braid.core.client.ui.styles.vars :as vars]
5 | [garden.units :refer [rem em px]]))
6 |
7 | (def me-page
8 | [:>.page.me
9 | (mixins/settings-style)
10 | [:>.content
11 | [:>.setting
12 | [:form.password
13 | [:label
14 | {:display "block"}]]
15 | [:>.nickname
16 | [:input.error
17 | {:border-color "red"}]]]]])
18 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/styles/pills.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.styles.pills
2 | (:require
3 | [braid.core.client.ui.styles.mixins :as mixins]))
4 |
5 | (defn tag []
6 | [:.tag
7 | [:>.pill
8 | mixins/pill-box]])
9 |
10 | (defn user []
11 | [:.user
12 | [:>.pill
13 | mixins/pill-box]])
14 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/styles/threads.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.styles.threads
2 | (:require
3 | [braid.core.client.ui.styles.thread]
4 | [braid.core.client.ui.styles.message]
5 | [braid.core.client.ui.styles.mixins :as mixins]
6 | [braid.core.client.ui.styles.vars :as vars]))
7 |
8 | (defn >threads []
9 | [:>.threads
10 | mixins/flex
11 | {:position "absolute"
12 | :top "2.6rem"
13 | :right 0
14 | :bottom 0
15 | :left 0
16 | :align-items "flex-end"
17 | :overflow-x "auto" }
18 |
19 | (braid.core.client.ui.styles.thread/thread vars/pad)
20 | (braid.core.client.ui.styles.thread/notice vars/pad)
21 |
22 | [:>.thread
23 |
24 | [:>.card
25 |
26 | [:>.messages
27 | braid.core.client.ui.styles.message/message]
28 |
29 | (braid.core.client.ui.styles.thread/new-message vars/pad)]]])
30 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/styles/vars.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.styles.vars
2 | (:require
3 | [garden.arithmetic :as m]
4 | [garden.units :refer [rem px]]))
5 |
6 | (def avatar-size (rem 2))
7 |
8 | (def pad (rem 1))
9 |
10 | (def card-width (rem 18))
11 |
12 | (def top-bar-height (rem 2))
13 |
14 | (def sidebar-width
15 | (m/+ top-bar-height (m/* pad 2)))
16 |
17 | (def grey-text "#888")
18 |
19 | (def darkgrey-text "#333")
20 |
21 | (def border-radius (px 3))
22 |
23 | (def private-thread-accent-color "#5f7997")
24 |
25 | (def limbo-thread-accent-color "#CA1414")
26 |
27 | (def archived-thread-accent-color "#AAAAAA")
28 |
29 | (def page-background-color "#f3f3f3")
30 | (def primary-text-color "#444444")
31 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/views/app.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.views.app
2 | (:require
3 | [braid.core.client.ui.views.main :refer [main-view]]
4 | [braid.base.client.styles :as base.styles]
5 | [braid.core.client.ui.views.styles :refer [styles-view]]
6 | [re-frame.core :refer [subscribe]]))
7 |
8 | (defn app-view []
9 | (case @(subscribe [:login-state])
10 |
11 | (:ws-connect :anon-ws-connect)
12 | [:div.status
13 | [base.styles/styles-view]
14 | [styles-view]
15 | [:span "Connecting..."]]
16 |
17 | (:gateway :app :anon-connected)
18 | [:div.app
19 | [base.styles/styles-view]
20 | [styles-view]
21 | [main-view]]))
22 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/views/card_border.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.views.card-border
2 | (:require
3 | [re-frame.core :refer [subscribe]]
4 | [braid.lib.color :as color]))
5 |
6 | (defn card-border-view [thread-id]
7 | [:div.border
8 | {:style
9 | {:background (color/->color @(subscribe [:open-group-id]))}}])
10 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/views/header_item.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.views.header-item
2 | (:require
3 | [re-frame.core :refer [subscribe]]
4 | [spec-tools.data-spec :as ds]))
5 |
6 | (def HeaderItem
7 | {(ds/opt :title) string?
8 | :priority number?
9 | (ds/opt :icon) string?
10 | (ds/opt :on-click) fn?
11 | (ds/opt :route-fn) fn?
12 | (ds/opt :route-args) {keyword? any?}
13 | (ds/opt :body) string?})
14 |
15 | (defn header-item-view
16 | [conf]
17 | (let [open-group-id (subscribe [:open-group-id])
18 | current-path (subscribe [:page-path])]
19 | (fn [{:keys [route-fn route-args title icon body on-click context]}]
20 | (let [path (when route-fn
21 | (route-fn (merge route-args {:group-id @open-group-id})))]
22 | [:a {:class (when (= path @current-path) "active")
23 | :href path
24 | :on-click (if context
25 | (on-click context)
26 | on-click)
27 | :title title}
28 | [:span body]
29 | (when icon
30 | [:div.icon icon])]))))
31 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/views/hover_menu.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.views.hover-menu
2 | (:require
3 | [braid.core.client.ui.views.header-item :refer [header-item-view]]))
4 |
5 | (defn hover-menu-view
6 | [context items position]
7 | [:div.hover-menu {:class (name position)}
8 | [:svg.arrow {:class (name position)
9 | :view-box "0 -5 10 10"}
10 | [:polyline {:points "0,-5 5,0 0,5"
11 | :transform (str "rotate("
12 | (case position
13 | :top 90
14 | :bottom 270
15 | :left 0
16 | :right 180)
17 | ", 5, 0)")}]]
18 | (into
19 | [:div.content]
20 | (doall
21 | (for [item (->> items
22 | (sort-by :priority)
23 | reverse)]
24 | ^{:key (item :class)}
25 | [header-item-view (assoc item :context context)])))])
26 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/views/main.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.views.main
2 | (:require
3 | [braid.core.client.gateway.views :refer [gateway-view]]
4 | [braid.core.client.gateway.forms.user-auth.views :refer [user-auth-view]]
5 | [braid.base.client.pages :as pages]
6 | [braid.base.client.root-view :as root-view]
7 | [braid.core.client.routes :as routes]
8 | [braid.core.client.ui.views.header :refer [header-view readonly-header-view]]
9 | [braid.core.client.ui.views.pages.readonly :refer [readonly-inbox-page-view]]
10 | [re-frame.core :refer [dispatch subscribe]]))
11 |
12 | (defn page-view []
13 | (let [page @(subscribe [:page])
14 | id (:type page)]
15 | (case id
16 | :readonly [readonly-inbox-page-view]
17 | :login [gateway-view]
18 | (when-let [view (pages/get-view id)]
19 | [view page]))))
20 |
21 | (defn readonly-page-view
22 | []
23 | (let [page @(subscribe [:page])
24 | id (:type page)]
25 | (case id
26 | (:inbox :readonly) [readonly-inbox-page-view]
27 | :login [gateway-view]
28 | (when-let [view (pages/get-view id)]
29 | [view page]))))
30 |
31 | (defn root-views []
32 | (into
33 | [:<>]
34 | (for [view @root-view/root-views]
35 | [view])))
36 |
37 | (defn main-view []
38 | (case @(subscribe [:login-state])
39 | :gateway
40 | [:div.main
41 | [:div.gateway
42 | [user-auth-view]]]
43 |
44 | :anon-connected
45 | [:div.main
46 | ;; TODO [sidebar-view]
47 | (when @(subscribe [:active-group])
48 | [readonly-header-view])
49 | [readonly-page-view]
50 | [root-views]]
51 |
52 | ;; default
53 | (into
54 | [:div.main
55 | (when @(subscribe [:open-group-id])
56 | [header-view])
57 | [page-view]
58 | [root-views]])))
59 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/views/mentions.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.views.mentions
2 | (:require
3 | [braid.popovers.helpers :as popover]
4 | [braid.core.client.ui.views.pills :refer [user-pill-view tag-pill-view]]
5 | [braid.core.client.ui.views.user-hover-card :refer [user-hover-card-view]]
6 | [braid.core.client.ui.views.tag-hover-card :refer [tag-hover-card-view]]))
7 |
8 | (defn user-mention-view
9 | [user-id]
10 | [:div.user
11 | {:style {:display "inline-block"}
12 | :on-mouse-enter
13 | (popover/on-mouse-enter
14 | (fn []
15 | [user-hover-card-view user-id]))}
16 | [user-pill-view user-id]])
17 |
18 | (defn tag-mention-view
19 | [tag-id]
20 | [:div.tag
21 | {:style {:display "inline-block"}
22 | :on-mouse-enter
23 | (popover/on-mouse-enter
24 | (fn []
25 | [tag-hover-card-view tag-id]))}
26 | [tag-pill-view tag-id]])
27 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/views/new_message_action_button.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.views.new-message-action-button
2 | (:require
3 | [braid.popovers.helpers :as popovers]
4 | [braid.core.client.ui.views.hover-menu :refer [hover-menu-view]]
5 | [braid.core.client.ui.views.header-item :refer [HeaderItem]]
6 | [braid.core.hooks :as hooks]))
7 |
8 | (defonce menu-items
9 | (hooks/register! (atom []) [HeaderItem]))
10 |
11 | (defn new-message-action-button-view
12 | [{:keys [thread-id group-id]}]
13 | [:div.plus
14 | (let [popup-view (fn [] [hover-menu-view
15 | {:thread-id thread-id
16 | :group-id group-id}
17 | @menu-items :top])]
18 | {:on-mouse-enter
19 | (popovers/on-mouse-enter popup-view)
20 | :on-touch-start
21 | (popovers/on-touch-start popup-view)})])
22 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/views/pages/changelog.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.views.pages.changelog
2 | (:require
3 | [braid.lib.xhr :as xhr]
4 | [reagent.core :as r]))
5 |
6 | (defn changelog-view []
7 | (let [change-hiccup (r/atom nil)]
8 | (fn []
9 | ; FIXME: circumvents re-frame, should use subscribe and dispatch
10 | (xhr/edn-xhr {:method :get
11 | :uri "/changelog"
12 | :on-complete
13 | (fn [resp]
14 | (reset! change-hiccup
15 | (or (:braid/ok resp)
16 | [:div.error "Failed to load changelog"])))})
17 | [:div.page.changelog
18 | [:div.title "Changelog"]
19 | [:div.content
20 | [:h1 "The Evolution of Braid"]
21 | (if-let [changes @change-hiccup]
22 | changes
23 | [:p "Loading..."])]])))
24 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/views/pages/readonly.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.views.pages.readonly
2 | (:require
3 | [braid.core.client.ui.views.header :refer [group-header-buttons-view]]
4 | [braid.core.client.ui.views.threads :refer [threads-view]]
5 | [braid.core.client.routes :as routes]
6 | [re-frame.core :refer [dispatch subscribe]]
7 | [reagent.ratom :include-macros true :refer-macros [reaction]]))
8 |
9 | (defn readonly-inbox-page-view
10 | []
11 | (let [group @(subscribe [:active-group])
12 | group-id @(subscribe [:open-group-id])
13 | open-threads @(subscribe [:open-threads])]
14 | [:div.page.readonly-inbox
15 | [:div.intro
16 | (:intro group)]
17 | [threads-view {:threads (map #(assoc % :readonly true) open-threads)
18 | :resort-nonce group-id}]]))
19 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/views/pills.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.views.pills
2 | (:require
3 | [re-frame.core :refer [dispatch subscribe]]
4 | [braid.lib.color :as color]))
5 |
6 | (defn tag-pill-view
7 | [tag-id]
8 | (let [tag (subscribe [:tag tag-id])
9 | user-subscribed-to-tag? (subscribe [:user-subscribed-to-tag? tag-id])
10 | color (color/->color tag-id)]
11 | [:span.pill {:class (if @user-subscribed-to-tag? "on" "off")
12 | :tab-index -1
13 | :style {:background-color color
14 | :color color
15 | :border-color color}}
16 | [:div.name "#" (@tag :name)]]))
17 |
18 | (defn user-pill-view
19 | [user-id]
20 | (let [user (or @(subscribe [:user user-id])
21 | {:nickname "DELETED"
22 | :status :offline})
23 | color (color/->color user-id)]
24 | [:span.pill {:class (str (case (user :status) :online "on" "off"))
25 | :tab-index -1
26 | :style {:background-color color
27 | :color color
28 | :border-color color}}
29 | [:span.name (str "@" (user :nickname))]]))
30 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/views/subscribe_button.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.views.subscribe-button
2 | (:require
3 | [re-frame.core :refer [dispatch subscribe]]))
4 |
5 | (defn subscribe-button-view
6 | [tag-id]
7 | (let [user-subscribed-to-tag? (subscribe [:user-subscribed-to-tag? tag-id])]
8 | (if @user-subscribed-to-tag?
9 | [:a.button {:on-click
10 | (fn [_]
11 | (dispatch [:unsubscribe-from-tag! tag-id]))}
12 | "Unsubscribe"]
13 | [:a.button {:on-click
14 | (fn [_]
15 | (dispatch [:subscribe-to-tag! {:tag-id tag-id}]))}
16 | "Subscribe"])))
17 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/views/tag_hover_card.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.views.tag-hover-card
2 | (:require
3 | [re-frame.core :refer [subscribe]]
4 | [braid.lib.color :as color]
5 | [braid.search.ui.search-button :refer [search-button-view]]
6 | [braid.core.client.ui.views.subscribe-button :refer [subscribe-button-view]]
7 | [braid.core.client.ui.views.pills :refer [tag-pill-view]]))
8 |
9 | (defn tag-hover-card-view
10 | [tag-id]
11 | (let [tag (subscribe [:tag tag-id])
12 | user-subscribed-to-tag? (subscribe [:user-subscribed-to-tag? tag-id])]
13 | [:div.tag.card
14 | [:div.header {:style {:background-color (color/->color tag-id)}}
15 | [tag-pill-view tag-id]
16 | [:div.subscribers.count
17 | {:title (str (@tag :subscribers-count) " Subscribers")}
18 | (@tag :subscribers-count)]
19 | [:div.threads.count
20 | {:title (str (@tag :threads-count) " Conversations")}
21 | (@tag :threads-count)]]
22 | [:div.info
23 | [:div.description
24 | (@tag :description)]]
25 | [:div.actions
26 | [search-button-view (str "#" (@tag :name))]
27 | [subscribe-button-view tag-id]]]))
28 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/views/thread.cljs.rej:
--------------------------------------------------------------------------------
1 | diff a/src/braid/core/client/ui/views/thread.cljs b/src/braid/core/client/ui/views/thread.cljs (rejected hunks)
2 | @@ -162,8 +162,7 @@
3 | (not (helpers/contains-urls? (prev-message :content)))))))))]
4 | (doall
5 | (for [message sorted-messages]
6 | - ^{:key (message :id)}
7 | - [:<>
8 | + [:<> {:key (message :id)}
9 | (when (message :show-date-divider?)
10 | [:div.divider
11 | [:div.date (helpers/format-date "yyyy-MM-dd" (message :created-at))]])
12 |
--------------------------------------------------------------------------------
/src/braid/core/client/ui/views/upload.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.core.client.ui.views.upload
2 | (:require
3 | [re-frame.core :refer [dispatch]]
4 | [reagent.core :as r]))
5 |
6 | (def max-avatar-size (* 2 1024 1024))
7 |
8 | (defn avatar-upload-view
9 | [args]
10 | (let [uploading? (r/atom false)
11 | start-upload (fn [on-upload group-id type file-list]
12 | (let [file (aget file-list 0)]
13 | (if (> (.-size file) max-avatar-size)
14 | (dispatch [:braid.notices/display! [:avatar-set-fail "Avatar image too large" :error]])
15 | (do (reset! uploading? true)
16 | (dispatch [:braid.uploads/upload!
17 | {:file file
18 | :group-id group-id
19 | :type type
20 | :on-complete
21 | (fn [{:keys [url]}]
22 | (reset! uploading? false)
23 | (on-upload url))}])))))]
24 | (fn [{:keys [on-upload dragging-change group-id type] :as args}]
25 | [:div.upload
26 | (if @uploading?
27 | [:div
28 | [:p "Uploading..." [:span.uploading-indicator "\uf110"]]]
29 | [:div
30 | {:on-drag-over (fn [e]
31 | (doto e (.stopPropagation) (.preventDefault))
32 | (dragging-change true))
33 | :on-drag-leave (fn [_] (dragging-change false))
34 | :on-drop (fn [e]
35 | (.preventDefault e)
36 | (dragging-change false)
37 | (reset! uploading? true)
38 | (start-upload on-upload group-id type (.. e -dataTransfer -files)))}
39 | [:label "Choose an avatar image"
40 | [:input {:type "file" :accept "image/*"
41 | :on-change (fn [e]
42 | (start-upload on-upload group-id type (.. e -target -files)))}]]])])))
43 |
--------------------------------------------------------------------------------
/src/braid/core/hooks.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.core.hooks
2 | "Provides a way to register atoms that can later all be reset to their initial values.
3 | This enables changes to Braid modules to be picked up in a reloaded workflow (ie. figwheel).
4 |
5 | On the client-side:
6 | Figwheel is configured to call (braid.core.client.desktop.core/reload!),
7 | which calls (braid.core.modules/init!), which calls (reset-all!)
8 |
9 | On the server-side:
10 | (braid.core.modules/init!) is called once, when the server is started (in braid.core)."
11 | (:require
12 | [braid.core.common.util :as util]))
13 |
14 | ;; Atoms is a map of atoms to their initial values
15 | (defonce atoms (atom {}))
16 |
17 | (defn register!
18 | "Takes an atom and adds it to a central registry so that it can be later reset to its initial value.
19 | Only the first registration of an atom is stored.
20 | Always returns the given atom."
21 | ([a data-spec]
22 | (when-not (contains? @atoms a)
23 | (swap! atoms assoc a @a))
24 | (when data-spec
25 | (set-validator! a (fn [x]
26 | ;; use assert, to elide the code in prod
27 | (assert (util/valid? data-spec x))
28 | ;; assert returns nil
29 | ;; but validator expects true
30 | true)))
31 | a)
32 | ([a]
33 | (register! a nil)))
34 |
35 | (defn reset-all!
36 | "Resets all registered atoms to their initial state."
37 | []
38 | (doseq [[a initial-state] @atoms]
39 | (reset! a initial-state)))
40 |
--------------------------------------------------------------------------------
/src/braid/core/server/core.clj:
--------------------------------------------------------------------------------
1 | (ns braid.core.server.core
2 | (:require
3 | [braid.core.server.middleware :refer [wrap-universal-middleware]]
4 | [braid.base.conf-extra :as conf] ; to update ports
5 | [braid.core.server.handler :as handler]
6 | [braid.base.server.ws-handler] ; for mount
7 | [braid.base.server.jobs] ; for mount
8 | [mount.core :as mount :refer [defstate]]
9 | [org.httpkit.server :refer [run-server server-port server-stop!]]
10 | [taoensso.timbre :as timbre]))
11 |
12 | ;; server
13 |
14 | (defn start-server! [{:keys [middleware port]}]
15 | (timbre/debugf "starting desktop server on port %d" port)
16 | (let [server (run-server (wrap-universal-middleware #'handler/app middleware)
17 | {:port port :legacy-return-value? false})
18 | desktop-port (server-port server)]
19 | (timbre/debugf "Started desktop server on port %d" desktop-port)
20 | (swap! conf/ports-config assoc
21 | :site-url (str "http://localhost:" (server-port server)))
22 | server))
23 |
24 | (defn stop-server!
25 | [srv]
26 | (timbre/debugf "stopping server")
27 | @(server-stop! srv {:timeout 100}))
28 |
29 | (defstate server
30 | :start (start-server! (mount/args))
31 | :stop (stop-server! server))
32 |
33 | ;; exceptions in background thread handler
34 |
35 | (defn set-default-exception-handler
36 | []
37 | (Thread/setDefaultUncaughtExceptionHandler
38 | (reify Thread$UncaughtExceptionHandler
39 | (uncaughtException [_ thread ex]
40 | (timbre/errorf ex "Uncaught exception on %s" (.getName thread))))))
41 |
42 | (defstate thread-handler
43 | :start (set-default-exception-handler))
44 |
--------------------------------------------------------------------------------
/src/braid/core/server/mail.clj:
--------------------------------------------------------------------------------
1 | (ns braid.core.server.mail
2 | "Sending email"
3 | (:require
4 | [braid.base.conf :as conf]
5 | [postal.core :as postal]))
6 |
7 | (defn- ->int [s]
8 | (try (Long. s)
9 | (catch java.lang.NumberFormatException _ nil)))
10 |
11 | (defn send!
12 | "Send an email message via SMTP using the server information in config.
13 |
14 | `body` can be a string, which will sent as plain text or a map of
15 | `:text` and `:html`, which will send a multipart/alternative message.
16 |
17 | Uses config variables:
18 | :email-host :email-user :email-password :email-port :email-from
19 | :email-secure can be the string `ssl` or `tls`
20 | "
21 | [{:keys [subject body to from]}]
22 | (postal/send-message
23 | (cond-> {:host (conf/config :email-host)
24 | :user (conf/config :email-user)
25 | :pass (conf/config :email-password)}
26 | (->int (conf/config :email-port)) (assoc :port (->int (conf/config :email-port)))
27 | (= "tls" (conf/config :email-secure)) (assoc :tls true)
28 | (= "ssl" (conf/config :email-secure)) (assoc :ssl true))
29 | {:from (or from (conf/config :email-from))
30 | :to to
31 | :subject subject
32 | :body (if (string? body)
33 | body
34 | [:alternative
35 | {:type "text/plain"
36 | :content (:text body)}
37 | {:type "text/html"
38 | :content (:html body)}])}))
39 |
--------------------------------------------------------------------------------
/src/braid/core/server/message_format.clj:
--------------------------------------------------------------------------------
1 | (ns braid.core.server.message-format
2 | (:require
3 | [clojure.string :as string]
4 | [braid.chat.db.group :as db.group]))
5 |
6 | (defn str->uuid
7 | [s]
8 | (java.util.UUID/fromString s))
9 |
10 | (defn make-tags-and-mentions-parser
11 | [group-id]
12 | (let [id->nick (into {} (map (juxt :id :nickname)) (db.group/group-users group-id))
13 | id->tag (into {} (map (juxt :id :name)) (db.group/group-tags group-id))]
14 | (fn [content]
15 | (let [uuid-re #"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"
16 | tag-re (re-pattern (str "#" uuid-re))
17 | mention-re (re-pattern (str "@" uuid-re))]
18 | (-> content
19 | (string/replace tag-re
20 | (comp (partial str "#") id->tag str->uuid second))
21 | (string/replace
22 | mention-re
23 | (comp (partial str "@") id->nick str->uuid second)))))))
24 |
--------------------------------------------------------------------------------
/src/braid/core/server/notify_rules.clj:
--------------------------------------------------------------------------------
1 | (ns braid.core.server.notify-rules
2 | (:require
3 | [clojure.set :as set]
4 | [braid.chat.db.tag :as tag]
5 | [braid.chat.db.thread :as thread]
6 | [braid.core.common.util :as util]
7 | [braid.core.common.schema :as schema]))
8 |
9 | (defn tag->group
10 | [tag-id]
11 | (tag/tag-group-id tag-id))
12 |
13 | (defn thread->tags
14 | [thread-id]
15 | (:tag-ids (thread/thread-by-id thread-id)))
16 |
17 | (defn thread->groups [thread-id]
18 | (into #{} (map tag->group) (thread->tags thread-id)))
19 |
20 | (defn notify?
21 | [user-id rules new-message]
22 | (assert (util/valid? schema/NotifyRules rules))
23 | (assert (util/valid? schema/NewMessage new-message))
24 | (let [{:keys [tag mention any]
25 | :or {tag #{} mention #{} any #{}}}
26 | (->> (group-by first rules)
27 | (into {}
28 | (map (fn [[k v]]
29 | [k (into #{} (map second) v)]))))]
30 | ; notify if...
31 | (or (any :any) ; ...you want to be notified by anything in any group
32 | ; ...or by a tag that this thread has
33 | (seq (set/intersection
34 | (set (new-message :mentioned-tag-ids))
35 | tag))
36 | (let [groups (thread->groups (new-message :thread-id))]
37 | (or ; ...or by anything in this group
38 | (seq (set/intersection groups any))
39 | ; ...or by a mention...
40 | (and (seq mention) ((set (new-message :mentioned-user-ids)) user-id)
41 | (or (mention :any) ; in any group
42 | ; or this group
43 | (seq (set/intersection
44 | groups
45 | mention)))))))))
46 |
--------------------------------------------------------------------------------
/src/braid/core/server/routes/helpers.clj:
--------------------------------------------------------------------------------
1 | (ns braid.core.server.routes.helpers
2 | (:require
3 | [braid.chat.db.user :as user]
4 | [ring.middleware.anti-forgery :as anti-forgery]))
5 |
6 | (defn logged-in? [req]
7 | (when-let [user-id (get-in req [:session :user-id])]
8 | (user/user-id-exists? user-id)))
9 |
10 | (defn current-user [req]
11 | (when-let [user-id (get-in req [:session :user-id])]
12 | (when (user/user-id-exists? user-id)
13 | (user/user-by-id user-id))))
14 |
15 | (defn current-user-id [req]
16 | (when-let [user-id (get-in req [:session :user-id])]
17 | (when (user/user-id-exists? user-id)
18 | user-id)))
19 |
20 | (defn session-token []
21 | anti-forgery/*anti-forgery-token*)
22 |
23 | (defn error-response [status msg]
24 | {:status status
25 | :headers {"Content-Type" "application/edn; charset=utf-8"}
26 | :body (pr-str {:error msg})})
27 |
28 | (defn edn-response [clj-body]
29 | {:headers {"Content-Type" "application/edn; charset=utf-8"}
30 | :body (pr-str clj-body)})
31 |
--------------------------------------------------------------------------------
/src/braid/core/server/routes/socket.clj:
--------------------------------------------------------------------------------
1 | (ns braid.core.server.routes.socket
2 | (:require
3 | [braid.base.server.socket :refer [ring-ajax-post ring-ajax-get-or-ws-handshake]]
4 | [compojure.core :refer [GET POST defroutes]]
5 | [ring.middleware.anti-forgery :refer [*anti-forgery-token*]]))
6 |
7 | (defroutes sync-routes
8 | (GET "/chsk" req
9 | (-> req
10 | (assoc-in [:session :ring.middleware.anti-forgery/anti-forgery-token]
11 | *anti-forgery-token*)
12 | (cond-> (and (not (get-in req [:session :user-id]))
13 | (not (get-in req [:session :fake-id])))
14 | (assoc-in [:session :fake-id] (str "FAKE" (java.util.UUID/randomUUID))))
15 | ring-ajax-get-or-ws-handshake))
16 | (POST "/chsk" req (ring-ajax-post req)))
17 |
--------------------------------------------------------------------------------
/src/braid/core/server/sync_handler.clj:
--------------------------------------------------------------------------------
1 | (ns braid.core.server.sync-handler
2 | (:require
3 | [braid.core.hooks :as hooks]
4 | [braid.core.server.sync-helpers :as helpers]
5 | [taoensso.timbre :as timbre :refer [debugf]]
6 | [braid.chat.db.group :as group]
7 | [braid.chat.db.thread :as thread]
8 | ;; FIXME: should do these view base.api/something
9 | [braid.base.server.ws-handler :refer [anon-msg-handler
10 | event-msg-handler]]))
11 |
12 | ;; anonymous event handlers
13 |
14 | (defonce anonymous-load-group (hooks/register! (atom []) [fn?]))
15 |
16 | (defmethod anon-msg-handler :braid.server.anon/load-group
17 | [{:as ev-msg :keys [event id ?data ring-req ?reply-fn send-fn]}]
18 | (when-let [group (group/group-by-id ?data)]
19 | (when (:public? group)
20 | (?reply-fn (reduce (fn [m f] (f (group :id) m))
21 | {:tags (group/group-tags ?data)
22 | :group group
23 | :threads (thread/public-threads ?data)}
24 | @anonymous-load-group))
25 | (helpers/add-anonymous-reader ?data (get-in ev-msg [:ring-req :session :fake-id])))))
26 |
27 | (defmethod anon-msg-handler :chsk/uidport-close
28 | [ev-msg]
29 | (debugf "Closing connection for anonymous client %s" (:client-id ev-msg))
30 | (helpers/remove-anonymous-reader (get-in ev-msg [:ring-req :session :fake-id])))
31 |
32 | (defmethod anon-msg-handler :braid.client/ping
33 | [{:as ev-msg :keys [?reply-fn]}]
34 | (when-let [reply ?reply-fn]
35 | (reply [:braid.server/pong])))
36 |
37 | ;; logged in event handlers
38 |
39 | (defmethod event-msg-handler :braid.client/ping
40 | [{:as ev-msg :keys [?reply-fn]}]
41 | (when-let [reply ?reply-fn]
42 | (reply [:braid.server/pong])))
43 |
--------------------------------------------------------------------------------
/src/braid/disconnect_notice/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.disconnect-notice.core
2 | (:require
3 | [braid.base.api :as base]
4 | #?@(:cljs
5 | [[braid.disconnect-notice.ui :as ui]
6 | [braid.disconnect-notice.styles :as styles]])))
7 |
8 | (defn init! []
9 | #?(:cljs
10 | (do
11 | (base/register-root-view! ui/disconnect-notice-view)
12 | (base/register-styles! [:#app>.app>.main styles/disconnect-notice]))))
13 |
--------------------------------------------------------------------------------
/src/braid/disconnect_notice/styles.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.disconnect-notice.styles
2 | (:require
3 | [braid.core.client.ui.styles.mixins :as mixins]
4 | [braid.core.client.ui.styles.vars :as vars]
5 | [garden.arithmetic :as m]))
6 |
7 | (def disconnect-notice
8 | [:>.disconnect-notice
9 | {:position "absolute"
10 | :bottom 0
11 | :left 0
12 | :right 0
13 | :top 0
14 | :background "rgba(242,242,242,0.75)"
15 | :z-index 10000}
16 |
17 | [:>.info
18 | {:position "absolute"
19 | :bottom vars/pad
20 | :height (m/* vars/pad 4)
21 | :left 0
22 | :right 0
23 | :padding vars/pad
24 | :box-sizing "border-box"
25 | :background "red"
26 | :color "white"
27 | :z-index 5001}
28 |
29 | [:>h1
30 | {:font-size "1em"
31 | :margin 0}]
32 |
33 | [:>.message
34 |
35 | [:>button
36 | (mixins/outline-button {:text-color "#fff"
37 | :border-color "#fff"
38 | :hover-text-color "#fff"
39 | :hover-border-color "fff"})]]]])
40 |
--------------------------------------------------------------------------------
/src/braid/disconnect_notice/ui.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.disconnect-notice.ui
2 | (:require
3 | [reagent.core :as r]
4 | [re-frame.core :refer [subscribe dispatch]]))
5 |
6 | (defn disconnect-notice-view []
7 | (let [websocket-state (subscribe [:core/websocket-state])
8 | seconds-to-reconnect (r/atom nil)]
9 | (fn []
10 | (let [tick! (fn []
11 | (reset! seconds-to-reconnect
12 | (js/Math.round
13 | (/ (- (@websocket-state :next-reconnect)
14 | (.valueOf (js/Date.)))
15 | 1000))))]
16 | (when-not (@websocket-state :connected?)
17 | [:div.disconnect-notice
18 | {:ref (fn [_]
19 | (tick!)
20 | (js/setTimeout (fn [_]
21 | (tick!))
22 | 1000))}
23 | [:div.info
24 | [:h1 "Uh oh! Braid has disconnected from the server."]
25 | [:div.message "Attempting to reconnect in " @seconds-to-reconnect " seconds... "
26 | [:button
27 | {:on-click (fn [_]
28 | (dispatch [:core/reconnect!]))}
29 | "Reconnect Now"]]]])))))
30 |
--------------------------------------------------------------------------------
/src/braid/embeds/api.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.embeds.api
2 | (:require
3 | [braid.base.api :as base]
4 | [braid.core.common.util :as util]
5 | #?@(:cljs
6 | [[braid.embeds.impl :as impl]])))
7 |
8 | #?(:cljs
9 | (do
10 | (defn register-embed!
11 | "Expects a map with:
12 | {:handler ___ - a fn that will return either nil or a vector of [view & args]
13 | :priority ___ - a number, higher means takes precedence before lower priority embed handlers
14 | :styles ___ - garden styles}
15 |
16 | The handler fn is passed a map:
17 | {:urls [\"http://example.org\"
18 | \"http://example.org/2\"]}}"
19 | [{:keys [handler styles priority] :as embed}]
20 | {:pre [(util/valid? impl/embed-engine-dataspec embed)]}
21 | (swap! impl/embed-engines conj embed)
22 | (when styles
23 | (base/register-styles! [:.embed styles])))))
24 |
--------------------------------------------------------------------------------
/src/braid/embeds/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.embeds.core
2 | "Allows for extending Braid with embed handlers, which can display inline content after a message, based on the content of the message"
3 | (:require
4 | [braid.base.api :as base]
5 | [braid.chat.api :as chat]
6 | #?@(:cljs
7 | [[braid.embeds.impl :as impl]
8 | [braid.embeds.styles :as styles]])))
9 |
10 | (defn init! []
11 | #?(:cljs
12 | (do
13 | (base/register-styles! styles/embed)
14 | (chat/register-footer-view! impl/embed-view))))
15 |
--------------------------------------------------------------------------------
/src/braid/embeds/impl.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.embeds.impl
2 | (:require
3 | [braid.core.hooks :as hooks]
4 | [spec-tools.data-spec :as ds]))
5 |
6 | (def url-re #"(http(?:s)?://\S+(?:\w|\d|/))")
7 |
8 | (defn- extract-urls
9 | "Given some text, returns a sequence of URLs contained in the text"
10 | [text]
11 | (map first (re-seq url-re text)))
12 |
13 | (def embed-engine-dataspec
14 | {:handler fn?
15 | (ds/opt :styles) vector?
16 | :priority number?})
17 |
18 | (defonce embed-engines
19 | (hooks/register! (atom []) [embed-engine-dataspec]))
20 |
21 | (defn embed-view [message]
22 | (when-let [handler (->> @embed-engines
23 | (sort-by :priority)
24 | reverse
25 | (some (fn [{:keys [handler]}]
26 | (handler {:urls (extract-urls (message :content))}))))]
27 | [:div.embed
28 | handler]))
29 |
--------------------------------------------------------------------------------
/src/braid/embeds/styles.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.embeds.styles
2 | (:require
3 | [garden.arithmetic :as m]
4 | [braid.core.client.ui.styles.vars :as vars]))
5 |
6 | (def embed
7 | [:.embed
8 | {:margin [[0 (m/* -1 vars/pad)]]
9 | :padding [[vars/pad 0 0]]
10 | :overflow "hidden"
11 | :cursor "pointer"}])
12 |
--------------------------------------------------------------------------------
/src/braid/embeds_image/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.embeds-image.core
2 | "If a message contains a link to an image, displays the image as an embed"
3 | (:require
4 | [braid.embeds.api :as embeds]))
5 |
6 | (defn image-embed-view
7 | [url]
8 | [:a.image
9 | {:href url
10 | :target "_blank"
11 | :rel "noopener noreferrer"}
12 | [:img {:src url}]])
13 |
14 | (defn init! []
15 | #?(:cljs
16 | (do
17 | (embeds/register-embed!
18 | {:handler
19 | (fn [{:keys [urls]}]
20 | (when-let [url (->> urls
21 | (some (fn [url]
22 | (re-matches #".*(png|jpg|jpeg|gif)$" url)))
23 | first)]
24 | [image-embed-view url]))
25 |
26 | :styles
27 | [:>.image
28 | [:>img
29 | {:width "100%"}]]
30 |
31 | :priority 1}))))
32 |
--------------------------------------------------------------------------------
/src/braid/embeds_map/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.embeds-map.core
2 | "Detects google maps links and includes embeds a static image"
3 | (:require
4 | [braid.base.api :as base]
5 | [braid.embeds.api :as embeds]
6 | #?@(:clj
7 | [[org.httpkit.client :as http]
8 | [braid.base.conf :refer [config]]]
9 | :cljs
10 | [[goog.object :as o]])))
11 |
12 | (def google-regex #"^https://www.google.com/maps/.*/@(\-?\d+\.\d+),(\-?\d+.\.\d+).*")
13 |
14 | #?(:cljs
15 | (defn- map-embed-view
16 | [url]
17 | (let [[_ lat lng] (re-matches google-regex url)]
18 | [:a
19 | {:href (str url)
20 | :target "_blank"
21 | :rel "noopener noreferrer"}
22 | [:img
23 | {:src (str "/api/maps-embeds/static-map?lat=" lat "&lng=" lng)}]])))
24 |
25 |
26 | (defn init! []
27 | #?(:clj
28 | (do
29 | (base/register-config-var! :google-maps-api-key :optional [:string])
30 |
31 | (base/register-private-http-route!
32 | [:get "/maps-embeds/static-map"
33 | (fn [request]
34 | (let [{:keys [lat lng]} (request :params)]
35 | (if-let [key (config :google-maps-api-key)]
36 | (let [resp @(http/request
37 | {:method :get
38 | :url (str "https://maps.googleapis.com/maps/api/staticmap?center=" lat "," lng "&key=" key "&size=300x200&zoom=16")})]
39 | {:status 200
40 | :headers {"Content-Type" (get-in resp [:headers :content-type])
41 | "Content-Length" (get-in resp [:headers :content-length])}
42 | :body (:body resp)})
43 | {:status 500
44 | :body nil})))]))
45 |
46 | :cljs
47 | (do
48 | (embeds/register-embed!
49 | {:handler
50 | (fn [{:keys [urls]}]
51 | (when-let [url (->> urls
52 | (some (fn [url]
53 | (first (re-matches google-regex url)))))]
54 | [map-embed-view url]))
55 |
56 | :styles
57 | [:>.image
58 | [:>img
59 | {:width "100%"}]]
60 |
61 | :priority 1}))))
62 |
--------------------------------------------------------------------------------
/src/braid/embeds_video/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.embeds-video.core
2 | "If a message contains a link to a video, displays the video as an embed"
3 | (:require
4 | [braid.embeds.api :as embeds]))
5 |
6 | (defn- video-embed-view
7 | [url]
8 | [:video
9 | {:src url
10 | :preload "metadata"
11 | :controls true}])
12 |
13 | (defn init! []
14 | #?(:cljs
15 | (do
16 | (embeds/register-embed!
17 | {:handler
18 | (fn [{:keys [urls]}]
19 | (when-let [url (->> urls
20 | (some (fn [url]
21 | (re-matches #".*(mp4|mkv|mov)$" url)))
22 | first)]
23 | [video-embed-view url]))
24 |
25 | :styles
26 | [:>video
27 | {:width "100%"}]
28 |
29 | :priority 1}))))
30 |
--------------------------------------------------------------------------------
/src/braid/embeds_website/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.embeds-website.core
2 | "If a message contains a link, displays a generic website embed"
3 | (:require
4 | [braid.base.api :as base]
5 | [braid.embeds.api :as embeds]
6 | #?@(:cljs
7 | [[braid.embeds-website.styles :as styles]
8 | [braid.embeds-website.views :as views]]
9 | :clj
10 | [[braid.embeds-website.link-extract :as link-extract]]))
11 | #?(:clj
12 | (:import
13 | (java.net URLDecoder))))
14 |
15 | (defn init! []
16 | #?(:cljs
17 | (do
18 | (embeds/register-embed!
19 | {:handler views/handler
20 | :styles styles/styles
21 | :priority -1}))
22 | :clj
23 | (do
24 | (base/register-public-http-route!
25 | [:get "/extract"
26 | (fn [request]
27 | (let [url (URLDecoder/decode (get-in request [:params :url]))]
28 | {:body (link-extract/extract url)}))]))))
29 |
--------------------------------------------------------------------------------
/src/braid/embeds_youtube/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.embeds-youtube.core
2 | "Detects youtube links and includes embedded video player"
3 | (:require
4 | [braid.embeds.api :as embeds]))
5 |
6 | (defn- youtube-embed-view
7 | [video-id]
8 | [:iframe
9 | {:src (str "https://www.youtube-nocookie.com/embed/" video-id "?rel=0")
10 | :frame-border 0
11 | :allow "encrypted-media; autoplay"
12 | :allow-full-screen true}])
13 |
14 | (defn init! []
15 | #?(:cljs
16 | (do
17 | (embeds/register-embed!
18 | {:handler
19 | (fn [{:keys [urls]}]
20 | (when-let [video-id (->> urls
21 | (some (fn [url]
22 | (or
23 | (second (re-matches #"^https?://youtu.be/(.*)" url))
24 | (second (re-matches #"^https?://www\.youtube\.com/watch\?v=(.*)" url))))))]
25 | [youtube-embed-view video-id]))
26 |
27 | :priority 1}))))
28 |
--------------------------------------------------------------------------------
/src/braid/emoji/api.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.emoji.api
2 | (:require
3 | [braid.base.api :as base]
4 | #?@(:cljs
5 | [[braid.emoji.client.lookup :as lookup]])))
6 |
7 | #?(:cljs
8 | (do
9 | (defn register-emoji!
10 | "Adds emoji, which will show up in autocomplete and in messages.
11 |
12 | Takes a map with three optional keys:
13 |
14 | :shortcode-lookup
15 | a 0-arity function that returns a map of shortcode->emoji-meta-map
16 | ex. (fn []
17 | {\":foobar:\" {:src \"https://example.com/foobar.png\"
18 | :class \"example-emoji\"}})
19 | the keys must have colons as prefixes and suffixes
20 | these will show up in autocomplete when the user types in a colon or bracket
21 | in messages and autocomplete, the :src and :class will be used in an image element
22 |
23 | :ascii-lookup
24 | a 0-arity function that returns a map of string->emoji-meta-map
25 | ex. (fn []
26 | {\":)\" {:src \"https://example.com/smiley.png\"
27 | :class \"example-emoji\"})
28 | these will not be used in autocomplete, but will be used in message
29 |
30 | :styles
31 | garden style rules for custom styling emoji"
32 | [{:keys [shortcode-lookup ascii-lookup styles]}]
33 | (when shortcode-lookup
34 | (swap! lookup/shortcode-fns conj shortcode-lookup))
35 | (when ascii-lookup
36 | (swap! lookup/ascii-fns conj ascii-lookup))
37 | (when styles
38 | (base/register-styles! styles)))))
39 |
--------------------------------------------------------------------------------
/src/braid/emoji/client/autocomplete.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.emoji.client.autocomplete
2 | (:require
3 | [clojure.string :as string]
4 | [braid.emoji.client.lookup :as lookup]
5 | [braid.emoji.client.views :refer [emoji-view]]))
6 |
7 | (defn- shortcode->brackets
8 | "We allow user to search for emoji either using colons or brackets, ie. :shortcode: or (shortcode).
9 | Internally, the lookups are stored with colon shortcodes, ie. :shortcode:
10 | If the user is searching with brackets, we replace the colons with brackets when displaying results."
11 | [shortcode]
12 | (let [base (apply str (-> shortcode rest butlast))]
13 | (str "(" base ")")))
14 |
15 | (defn- emoji-result-view
16 | [shortcode emoji-meta show-as-brackets?]
17 | [:div.emoji.match
18 | [emoji-view shortcode emoji-meta]
19 | [:div.info
20 | [:div.name (if show-as-brackets?
21 | (shortcode->brackets shortcode)
22 | shortcode)]
23 | [:div.extra]]])
24 |
25 | (defn autocomplete-handler [text]
26 | (let [pattern #"\B[:(](\S{2,})$"]
27 | (when-let [query (second (re-find pattern text))]
28 | (let [show-as-brackets? (= "(" (first text))]
29 | (->> (lookup/shortcode)
30 | (filter (fn [[shortcode _]]
31 | (string/includes? shortcode query)))
32 | (map (fn [[shortcode emoji-meta]]
33 | {:key
34 | (fn [] shortcode)
35 | :action
36 | (fn [])
37 | :message-transform
38 | (fn [text]
39 | (string/replace text pattern (str shortcode " ")))
40 | :html
41 | (fn []
42 | [emoji-result-view shortcode emoji-meta show-as-brackets?])})))))))
43 |
--------------------------------------------------------------------------------
/src/braid/emoji/client/lookup.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.emoji.client.lookup
2 | (:require
3 | [braid.core.hooks :as hooks]))
4 |
5 | (defonce shortcode-fns
6 | (hooks/register! (atom []) [fn?]))
7 |
8 | (defonce ascii-fns
9 | (hooks/register! (atom []) [fn?]))
10 |
11 | (defn shortcode []
12 | (->> @shortcode-fns
13 | (map (fn [f] (f)))
14 | (apply merge {})))
15 |
16 | (defn ascii []
17 | (->> @ascii-fns
18 | (map (fn [f] (f)))
19 | (apply merge {})))
20 |
--------------------------------------------------------------------------------
/src/braid/emoji/client/styles.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.emoji.client.styles)
2 |
3 | (def autocomplete
4 | [:.app
5 | [:.message>.content
6 | [:.emoji.custom-emoji
7 | {:width "3ex"
8 | :height "3.1ex"
9 | :min-height "20px"
10 | :display "inline-block"
11 | :margin [["-0.2ex" "0.15em" "0.2ex"]]
12 | :line-height "normal"
13 | :vertical-align "middle"
14 | :min-width "20px"}]]
15 | [:>.main>.page>.threads>.thread>.card
16 | [:>.message.new
17 | [:>.autocomplete-wrapper
18 | [:>.autocomplete
19 | [:>.result
20 | [:>.emoji.match
21 | [:>img
22 | {:display "block"
23 | :width "2em"
24 | :height "2em"
25 | :float "left"
26 | :margin "0.25em 0.5em 0.25em 0"}]]]]]]]])
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/braid/emoji/client/text_replacements.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.emoji.client.text-replacements
2 | (:require
3 | [clojure.string :as string]
4 | [braid.emoji.client.lookup :as lookup]
5 | [braid.emoji.client.views :refer [emoji-view]]))
6 |
7 | (defn emoji-shortcodes-replace
8 | [node]
9 | (if (string? node)
10 | (if-let [match (re-matches #":\S*:" node)]
11 | (if-let [emoji-meta ((lookup/shortcode) match)]
12 | [emoji-view match emoji-meta]
13 | node)
14 | node)
15 | node))
16 |
17 | (defn emoji-ascii-replace [node]
18 | (if (string? node)
19 | (if-let [emoji-meta ((lookup/ascii) node)]
20 | [emoji-view node emoji-meta]
21 | node)
22 | node))
23 |
--------------------------------------------------------------------------------
/src/braid/emoji/client/views.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.emoji.client.views)
2 |
3 | (defn emoji-view [shortcode emoji-meta]
4 | [:img {:class ["emoji" (emoji-meta :class)]
5 | :alt shortcode
6 | :title shortcode
7 | :src (emoji-meta :src)}])
8 |
--------------------------------------------------------------------------------
/src/braid/emoji/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.emoji.core
2 | "Allows other modules to define emoji"
3 | (:require
4 | [braid.base.api :as base]
5 | [braid.chat.api :as chat]
6 | #?@(:cljs
7 | [[braid.emoji.client.autocomplete :refer [autocomplete-handler]]
8 | [braid.emoji.client.text-replacements :refer [emoji-ascii-replace
9 | emoji-shortcodes-replace]]
10 | [braid.emoji.client.styles :refer [autocomplete]]])))
11 |
12 | (defn init! []
13 | #?(:cljs
14 | (do
15 | (chat/register-autocomplete-engine! autocomplete-handler)
16 | (chat/register-message-transform! emoji-ascii-replace)
17 | (chat/register-message-transform! emoji-shortcodes-replace)
18 | (base/register-styles! autocomplete))))
19 |
--------------------------------------------------------------------------------
/src/braid/emoji_big/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.emoji-big.core
2 | "If a message consists of a single emoji, displays it larger."
3 | (:require
4 | [braid.chat.api :as chat]
5 | #?@(:cljs
6 | [[braid.emoji.client.views :refer [emoji-view]]])))
7 |
8 | #?(:cljs
9 | (do
10 | (defn- emoji? [node]
11 | (and
12 | (vector? node)
13 | (= emoji-view (first node))))
14 |
15 | (defn- single-emoji? [message]
16 | (let [emoji-count (->> message
17 | (filter emoji?)
18 | count)
19 | other-count (- (count message) emoji-count)]
20 | (and
21 | (= emoji-count 1)
22 | (= other-count 0))))
23 |
24 | (defn- embiggen-emoji [message]
25 | (for [node message]
26 | (if (emoji? node)
27 | (update-in node [2 :class] #(str % " large"))
28 | node)))))
29 |
30 | (defn init! []
31 | #?(:cljs
32 | (do
33 | (chat/register-message-formatter!
34 | (fn [message]
35 | (if (single-emoji? message)
36 | (embiggen-emoji message)
37 | message))))))
38 |
--------------------------------------------------------------------------------
/src/braid/emoji_custom/client/autocomplete.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.emoji-custom.client.autocomplete
2 | (:require
3 | [clojure.string :as string]
4 | [re-frame.core :refer [subscribe]]
5 | [braid.lib.upload :as upload]))
6 |
7 | (defn lookup []
8 | (if-let [emoji @(subscribe [:custom-emoji/group-emojis])]
9 | (->> emoji
10 | (reduce (fn [memo {:keys [image shortcode]}]
11 | (assoc memo shortcode {:class "custom-emoji"
12 | :src (upload/->path image)})) {})
13 | (into {}))
14 | {}))
15 |
--------------------------------------------------------------------------------
/src/braid/emoji_custom/client/styles.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.emoji-custom.client.styles
2 | (:require
3 | [braid.core.client.ui.styles.mixins :as mixins]))
4 |
5 | (def settings-page
6 | [:.app>.main>.page.group-settings>.content
7 | [:.settings.custom-emoji
8 | [:button.delete
9 | {:color "red"}
10 | (mixins/fontawesome nil)]]])
11 |
--------------------------------------------------------------------------------
/src/braid/emoji_custom/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.emoji-custom.core
2 | "Allows group admins to create custom emoji"
3 | (:require
4 | [braid.base.api :as base]
5 | [braid.chat.api :as chat]
6 | #?@(:cljs
7 | [[braid.emoji.api :as emoji]
8 | [braid.emoji-custom.client.autocomplete :as autocomplete]
9 | [braid.emoji-custom.client.styles :refer [settings-page]]
10 | [braid.emoji-custom.client.state :as state]
11 | [braid.emoji-custom.client.views :refer [extra-emoji-settings-view]]]
12 | :clj
13 | [[braid.emoji-custom.server.db :refer [db-schema]]
14 | [braid.emoji-custom.server.core :refer [initial-user-data-fn
15 | server-message-handlers]]])))
16 |
17 | (defn init! []
18 | #?(:cljs
19 | (do
20 | (emoji/register-emoji!
21 | {:shortcode-lookup autocomplete/lookup})
22 | (base/register-styles! settings-page)
23 | (base/register-state! state/initial-state state/state-spec)
24 | (base/register-initial-user-data-handler! state/initial-data-handler)
25 | (chat/register-group-setting! extra-emoji-settings-view)
26 | (base/register-events! state/events)
27 | (base/register-subs! state/subscriptions)
28 | (base/register-incoming-socket-message-handlers!
29 | state/socket-message-handlers))
30 |
31 | :clj
32 | (do
33 | (base/register-db-schema! db-schema)
34 | (base/register-initial-user-data! initial-user-data-fn)
35 | (base/register-server-message-handlers! server-message-handlers))))
36 |
--------------------------------------------------------------------------------
/src/braid/emoji_emojione/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.emoji-emojione.core
2 | "Provides emojione-style emoji"
3 | (:require
4 | [braid.emoji.api :as emoji]
5 | #?@(:cljs
6 | [[braid.emoji-emojione.impl :as impl]])))
7 |
8 | (defn init! []
9 | #?(:cljs
10 | (do
11 | (emoji/register-emoji!
12 | {:shortcode-lookup impl/shortcode-lookup
13 | :ascii-lookup impl/ascii-lookup
14 | :styles impl/emojione-styles}))))
15 |
--------------------------------------------------------------------------------
/src/braid/emoji_emojione/impl.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.emoji-emojione.impl
2 | (:require
3 | [garden.units :refer [rem em px ex]]
4 | [braid.emoji-emojione.ref :as refs]))
5 |
6 | ;; Taken from https://github.com/emojione/emojione/blob/2.2.7/assets/css/emojione.css#L1
7 | (def emojione-styles
8 | [:.emojione
9 | {:font-size "inherit"
10 | :height (ex 3)
11 | :width (ex 3.1)
12 | :min-height (px 20)
13 | :min-width (px 20)
14 | :display "inline-block"
15 | :margin [[(ex -0.2) (em 0.15) (ex 0.2)]]
16 | :line-height "normal"
17 | :vertical-align "middle"}
18 | ;; Taken from https://github.com/emojione/emojione/blob/2.2.7/assets/css/emojione-awesome.css#L24
19 | [:&.large
20 | {:width (em 3)
21 | :height (em 3)
22 | :margin [[(ex -0.6) (em 0.15) 0 (em 0.3)]]
23 | :background-size [[(em 3) (em 3)]]}]])
24 |
25 | (defn- emoji-meta [url]
26 | {:class "emojione"
27 | :src url})
28 |
29 | (defn- emojione-url [id]
30 | (str "//cdn.jsdelivr.net/emojione/assets/png/" id ".png"))
31 |
32 | (defn shortcode-lookup []
33 | (->> refs/shortcodes
34 | (reduce (fn [memo [shortcode ids]]
35 | (assoc memo shortcode
36 | (emoji-meta (emojione-url (last ids))))) {})))
37 |
38 | (defn ascii-lookup []
39 | (->> refs/ascii
40 | (reduce (fn [memo [ascii shortcode]]
41 | (let [id (last (refs/shortcodes shortcode))]
42 | (assoc memo ascii
43 | (emoji-meta (emojione-url id))))) {})))
44 |
--------------------------------------------------------------------------------
/src/braid/group_create/test.clj:
--------------------------------------------------------------------------------
1 | ;; doesn't work
2 | {::foo
3 | (fn []
4 | {})}
5 |
6 | ;; does work
7 | {::foo
8 | (fn [])}
9 |
10 | {:foo
11 | (fn []
12 | {})}
13 |
14 | {:foo
15 | (identity
16 | {})}
17 |
--------------------------------------------------------------------------------
/src/braid/group_create/validations.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.group-create.validations
2 | (:require
3 | [clojure.string :as string]
4 | [braid.lib.xhr :as xhr]))
5 |
6 | (def validations
7 | {:group-name
8 | [(fn [name cb]
9 | (if (string/blank? name)
10 | (cb "Your group needs a name.")
11 | (cb nil)))]
12 |
13 | :group-url
14 | [(fn [url cb]
15 | (if (string/blank? url)
16 | (cb "Your group needs a URL.")
17 | (cb nil)))
18 | (fn [url cb]
19 | (if (not (re-matches #"[a-z0-9-]*" url))
20 | (cb "Your URL can only contain lowercase letters, numbers or dashes.")
21 | (cb nil)))
22 | (fn [url cb]
23 | (if (re-matches #"-.*" url)
24 | (cb "Your URL can't start with a dash.")
25 | (cb nil)))
26 | (fn [url cb]
27 | (if (re-matches #".*-" url)
28 | (cb "Your URL can't end with a dash.")
29 | (cb nil)))
30 | (fn [url cb]
31 | (xhr/edn-xhr
32 | {:uri "/registration/check-slug-unique"
33 | :method :get
34 | :params {:slug url}
35 | :on-complete (fn [valid?]
36 | (if valid?
37 | (cb nil)
38 | (cb "Your group URL is already taken; try another.")))
39 | :on-error (fn [_]
40 | (cb "There was an error checking your URL."))}))]
41 |
42 | :group-type
43 | [(fn [type cb]
44 | (if (string/blank? type)
45 | (cb "You need to select a group type.")
46 | (cb nil)))]})
47 |
--------------------------------------------------------------------------------
/src/braid/group_explore/api.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.group-explore.api
2 | #?(:cljs
3 | (:require
4 | [braid.group-explore.views :as views])))
5 |
6 | #?(:cljs
7 | (defn register-link!
8 | [{:keys [title url] :as link}]
9 | (swap! views/group-explore-links conj link)))
10 |
--------------------------------------------------------------------------------
/src/braid/group_explore/styles.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.group-explore.styles
2 | (:require
3 | [braid.core.client.ui.styles.mixins :as mixins]
4 | [braid.core.client.ui.styles.vars :as vars]
5 | [garden.units :refer [rem em px]]))
6 |
7 | (def >group-explore-page
8 | [:>.page.group-explore
9 |
10 | [:>.content
11 | (mixins/settings-container-style)
12 | [:>.invitations
13 | (mixins/settings-item-style)]
14 | [:>.public-groups
15 | (mixins/settings-item-style)
16 |
17 | [:>.active
18 | :>.stale
19 | {:display "flex"
20 | :flex-wrap "wrap"}
21 |
22 | [:>a.group
23 | {:color "white"
24 | :padding "0.5em"
25 | :margin "0.2em"
26 | :border-radius (px 5)
27 | :text-decoration "none"}
28 | [:&:hover
29 | {:color "#ccc"
30 | :opacity 0.8}]
31 |
32 | [:>.name
33 | {:font-size "large"}]
34 |
35 | [:>.info
36 | {:font-size "small"}]]]
37 |
38 | [:>h3
39 |
40 | [:>button.toggle-stale
41 | mixins/standard-font
42 | {:background "transparent"
43 | :background-color "darkgray"
44 | :cursor "pointer"
45 | :border "none"
46 | :color "white"
47 | :margin-left "1rem"}]]]]])
48 |
--------------------------------------------------------------------------------
/src/braid/lib/color.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.lib.color
2 | (:require
3 | [clojure.string :as string]
4 | [cljsjs.husl]
5 | [braid.lib.url :as url]))
6 |
7 | (defn ->color [input]
8 | (js/window.HUSL.toHex (mod (Math/abs (hash input)) 360) 95 50))
9 |
10 | (defn ->color-darker [input]
11 | (js/window.HUSL.toHex (mod (Math/abs (hash input)) 360) 95 30))
12 |
13 | (defn url->color [url]
14 | (-> url
15 | string/lower-case
16 | url/url->parts
17 | :domain
18 | ->color))
19 |
--------------------------------------------------------------------------------
/src/braid/lib/date.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.lib.date
2 | (:require
3 | [cljs-time.core :as t]
4 | [cljs-time.format :as f]))
5 |
6 | (defn format-date
7 | [format-string datetime]
8 | (f/unparse (f/formatter format-string) (t/to-default-time-zone datetime)))
9 |
10 | (defn smart-format-date
11 | "Turn a Date object into a nicely formatted string"
12 | [datetime]
13 | (let [datetime (t/to-default-time-zone datetime)
14 | now (t/to-default-time-zone (t/now))
15 | format (cond
16 | (= (f/unparse (f/formatter "yyyydM") now)
17 | (f/unparse (f/formatter "yyyydM") datetime))
18 | "h:mm A"
19 |
20 | (= (t/year now) (t/year datetime))
21 | "h:mm A MMM d"
22 |
23 | :else
24 | "h:mm A MMM d yyyy")]
25 | (f/unparse (f/formatter format) datetime)))
26 |
--------------------------------------------------------------------------------
/src/braid/lib/digest.clj:
--------------------------------------------------------------------------------
1 | (ns braid.lib.digest
2 | (:require
3 | [clojure.java.io :as io])
4 | (:import
5 | [java.security MessageDigest]
6 | [org.apache.commons.codec.binary Base64]))
7 |
8 | (defn sha256-digest
9 | "Byte array to sha256"
10 | [^bytes bs]
11 | (.digest (doto (MessageDigest/getInstance "SHA-256") (.update bs))))
12 |
13 | (defn from-file
14 | [f]
15 | (when-let [file (io/resource f)]
16 | (-> file
17 | slurp
18 | .getBytes
19 | sha256-digest
20 | Base64/encodeBase64
21 | String.)))
22 |
--------------------------------------------------------------------------------
/src/braid/lib/gravatar.clj:
--------------------------------------------------------------------------------
1 | (ns braid.lib.gravatar
2 | (:require
3 | [clojure.string :as string]
4 | [braid.lib.crypto :as crypto]))
5 |
6 | (def ^:private gravatar-base-url ".gravatar.com/avatar/")
7 |
8 | (defn- genhash
9 | "Generate the hash needed for Gravatar.
10 | The hash is generated by trimming, lowercasing & md5-summing the email."
11 | [^String email]
12 | (-> email string/trim string/lower-case crypto/md5 crypto/bytes->hex
13 | string/lower-case))
14 |
15 | (defn- genparams
16 | "Generate the parameters needed for Gravatar."
17 | [& {size :size default :default rating :rating}]
18 | (format "?s=%s&r=%s&d=%s&" size (name rating) (name default)))
19 |
20 | (defn url
21 | "Generate a gravatar url.
22 | Based on https://github.com/Raynes/clavatar, updated to not use
23 | java.xml.bind, to avoid java 9+ module issues."
24 | [email & {:keys [size https rating default]
25 | :or {size 50
26 | https true
27 | rating :pg
28 | default :retro}}]
29 | (let [base-url (str gravatar-base-url
30 | (genhash (or email ""))
31 | ".jpg"
32 | (genparams :size size :default default :rating rating))]
33 | (str (if https "https://secure" "http://www") base-url)))
34 |
--------------------------------------------------------------------------------
/src/braid/lib/markdown.clj:
--------------------------------------------------------------------------------
1 | (ns braid.lib.markdown
2 | (:require
3 | [clojure.string :as string]
4 | [instaparse.core :as insta]))
5 |
6 | ; TODO: refactor this grammar to remove ambiguities - currently issues where
7 | ; PLAIN_TEXT is ambiguous, since you can have [:PLAIN_TEXT "foo"] or
8 | ; [:PLAIN_TEXT "f"] [:PLAIN_TEXT "oo"], etc
9 | (def markdown-parser
10 | "Simple markdown parser. Only parsing enough to handle CHANGELOG.md, so lots
11 | is probably missing"
12 | (insta/parser
13 | "S ::= ( ( HEADER | LIST | ) / PARAGRAPH ) *
14 |
15 | ::= #'(?m)\\A|^'
16 | ::= #'\\n|\\z'
17 | ws ::= #'[ \\t\\x0b]*'
18 |
19 | ::= #'.'
20 | PLAIN_TEXT ::= ( !LINK DOT ) +
21 | URL ::= #'\\S' +
22 | LINK ::= <'['> PLAIN_TEXT <']('> URL <')'>
23 | ::= ( LINK / PLAIN_TEXT ) ( TEXT ?)
24 |
25 | HEADER ::= #'#+' TEXT <'#'*>
26 |
27 | LIST ::= ( LIST_LINE ) +
28 | LIST_LINE ::= <#'\\s+(-|\\*)'> TEXT
29 |
30 | PARAGRAPH ::= PLAIN_LINE+
31 | BLANK_LINE ::= STARTL ws ENDL
32 | ::= !BLANK_LINE TEXT
33 | "))
34 |
35 | (defn markdown->hiccup
36 | "Parse markdown into hiccup."
37 | [md-str]
38 | (->> (markdown-parser md-str)
39 | (insta/transform {:HEADER (fn [delim & rst]
40 | (vec (cons (keyword (str "h" (count delim)))
41 | rst)))
42 | :PLAIN_TEXT (fn [& args] (string/join "" args))
43 | :URL (fn [& args] (string/join "" args))
44 | :LINK (fn [title url]
45 | [:a {:href url} title])
46 | :LIST (fn [& args]
47 | (vec (cons :ul args)))
48 | :LIST_LINE (fn [& args]
49 | (vec (cons :li args)))
50 | :PARAGRAPH (fn [& lines]
51 | (vec (cons :p lines)))
52 | :S (fn [& args]
53 | (vec (cons :div args)))})))
54 |
--------------------------------------------------------------------------------
/src/braid/lib/misc.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.lib.misc)
2 |
3 | (defn key-by [k coll]
4 | (into {} (map (juxt k identity)) coll))
5 |
6 | (defn flip
7 | "Partially apply the function f to the given args, which will come after the
8 | next args. i.e. ((flip vector 3 4) 1 2) => [1 2 3 4]"
9 | [f & a]
10 | (fn [& b]
11 | (apply f (concat b a))))
12 |
--------------------------------------------------------------------------------
/src/braid/lib/noembed.clj:
--------------------------------------------------------------------------------
1 | (ns braid.lib.noembed
2 | (:require [clojure.data.json :as json]
3 | [org.httpkit.client :as http]
4 | [taoensso.timbre :as timbre]))
5 |
6 | (defn get-oembed
7 | [url]
8 | (-> @(http/request {:url "http://noembed.com/embed"
9 | :query-params {:url url}})
10 | :body
11 | (json/read-str :key-fn keyword)))
12 |
--------------------------------------------------------------------------------
/src/braid/lib/s3.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.lib.s3
2 | (:require
3 | [braid.lib.xhr :refer [edn-xhr ajax-xhr]]
4 | [taoensso.timbre :as timbre :refer-macros [errorf]]))
5 |
6 | (defn s3-path [bucket region & paths]
7 | (str "https://" bucket ".s3." region ".amazonaws.com/" (apply str paths)))
8 |
9 | (defn upload
10 | [{:keys [file prefix on-complete]}]
11 | (edn-xhr
12 | {:method :get
13 | :uri "/s3-policy"
14 | :on-error (fn [err]
15 | (errorf "Error getting s3 authorization: %s" err))
16 | :on-complete
17 | (fn [{:keys [bucket region auth]}]
18 | (let [file-name (.-name file)
19 | file-key (str prefix (js/encodeURIComponent file-name))
20 | file-url (s3-path bucket region file-key)]
21 | (ajax-xhr {:method :post
22 | :uri (s3-path bucket region)
23 | :body (doto (js/FormData.)
24 | (.append "key" (str prefix "${filename}"))
25 | (.append "acl" "private")
26 | (.append "policy" (:policy auth))
27 | (.append "x-amz-algorithm" "AWS4-HMAC-SHA256")
28 | (.append "x-amz-credential" (:credential auth))
29 | (.append "x-amz-signature" (:signature auth))
30 | (.append "x-amz-date" (:date auth))
31 | (cond->
32 | (:security-token auth)
33 | (.append "x-amz-security-token"
34 | (:security-token auth)))
35 | (.append "Content-Type" (.-type file))
36 | (.append "file" file file-name))
37 | :on-complete (fn [_]
38 | (on-complete {:key file-key
39 | :url file-url}))
40 | :on-error (fn [e] (errorf "Error uploading: %s" (:error e)))})))}))
41 |
--------------------------------------------------------------------------------
/src/braid/lib/transit.clj:
--------------------------------------------------------------------------------
1 | (ns braid.lib.transit
2 | (:require
3 | [cognitect.transit :as transit])
4 | (:import
5 | (java.io ByteArrayOutputStream ByteArrayInputStream)
6 | (com.cognitect.transit Writer)
7 | (com.cognitect.transit.impl MsgpackEmitter WriteCache WriteHandlerMap)
8 | (org.msgpack MessagePack)))
9 |
10 | (defn fake-cache
11 | []
12 | (proxy [WriteCache] []
13 | (isCacheable [_ _] false)
14 | (cacheWrite [s _] s)))
15 |
16 | (defn noncaching-writer
17 | [^java.io.OutputStream out]
18 | (let [handlers (WriteHandlerMap. ^java.util.Map transit/default-write-handlers)
19 | packer (.createPacker (MessagePack.) out)
20 | emitter (MsgpackEmitter. packer handlers)
21 | write-cache (fake-cache)
22 | wrtr (reify Writer
23 | (write [_ o]
24 | (try
25 | (.emit emitter o false (.init write-cache))
26 | (.flush out)
27 | (catch Throwable e
28 | (throw (RuntimeException. e))))))]
29 | (transit/->Writer wrtr)))
30 |
31 | (defn ->transit ^bytes
32 | [form]
33 | (let [out (ByteArrayOutputStream. 4096)
34 | ;; writer (transit/writer out :msgpack)
35 | writer (noncaching-writer out)]
36 | (transit/write writer form)
37 | (.toByteArray out)))
38 |
39 | (defn transit->form
40 | [^bytes ts]
41 | (let [in (ByteArrayInputStream. ts)]
42 | (transit/read (transit/reader in :msgpack))))
43 |
--------------------------------------------------------------------------------
/src/braid/lib/upload.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.lib.upload
2 | (:require
3 | [clojure.string :as string]
4 | [goog.object :as o]))
5 |
6 | (def regex (delay
7 | (re-pattern (str "https?://"
8 | (-> (o/get js/window "asset_domain")
9 | (string/replace "." "\\."))
10 | "/"))))
11 |
12 | (defn ->path [url]
13 | (some-> url
14 | (string/replace
15 | @regex
16 | "/api/upload/")))
17 |
18 | (defn upload-path? [url]
19 | (string/includes? url (o/get js/window "asset_domain")))
20 |
--------------------------------------------------------------------------------
/src/braid/lib/url.clj:
--------------------------------------------------------------------------------
1 | (ns braid.lib.url
2 | (:require
3 | [clojure.string :as string])
4 | (:import
5 | (java.net URLEncoder)
6 | (org.apache.commons.validator UrlValidator)))
7 |
8 | (defn valid-url?
9 | "Check if the string is a valid http(s) url. Note that this will *not*
10 | accept local urls such as localhost or my-machine.local"
11 | ([s] (valid-url? s ["http" "https"]))
12 | ([s schemes]
13 | (and (string? s)
14 | (or
15 | (string/starts-with? s "http://localhost:")
16 | (.isValid (UrlValidator. (into-array schemes)) s)))))
17 |
18 | (defn map->query-str
19 | [m]
20 | (->> m
21 | (map (fn [[k v]] (str (name k) "=" (URLEncoder/encode (str v)))))
22 | (string/join "&")))
23 |
24 |
--------------------------------------------------------------------------------
/src/braid/lib/url.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.lib.url
2 | (:import
3 | (goog Uri)))
4 |
5 | (def url-re #"(http(?:s)?://\S+(?:\w|\d|/))")
6 |
7 | (defn extract-urls
8 | "Given some text, returns a sequence of URLs contained in the text"
9 | [text]
10 | (map first (re-seq url-re text)))
11 |
12 | (defn contains-urls? [text]
13 | (boolean (seq (extract-urls text))))
14 |
15 | (defn url->parts [url]
16 | (let [url-info (.parse Uri url)]
17 | {:domain (.getDomain url-info)
18 | :path (.getPath url-info)
19 | :scheme (.getScheme url-info)
20 | :port (.getPort url-info)}))
21 |
22 | (defn site-url
23 | []
24 | (let [{:keys [domain scheme port]} (url->parts (.-location js/window))]
25 | (str scheme "://" domain (when (or (and (= scheme "http")
26 | (not= port 80))
27 | (and (= scheme "https")
28 | (not= port 443)))
29 | (str ":" port)))))
30 |
--------------------------------------------------------------------------------
/src/braid/lib/uuid.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.lib.uuid
2 | (:require
3 | #?@(:clj
4 | [[datomic.api :as d]]
5 | :cljs
6 | [[cljs-uuid-utils.core :as uuid]])))
7 |
8 | (defn squuid []
9 | #?(:clj
10 | (d/squuid)
11 | :cljs
12 | (uuid/make-random-squuid)))
13 |
--------------------------------------------------------------------------------
/src/braid/lib/xhr.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.lib.xhr
2 | (:require
3 | [ajax.core :refer [ajax-request json-request-format json-response-format]]
4 | [ajax.edn :refer [edn-request-format edn-response-format]]
5 | [goog.object :as o]))
6 |
7 | (defn edn-xhr
8 | [args]
9 | (ajax-request (assoc args
10 | :uri (str "/api" (args :uri))
11 | :with-credentials true
12 | :format (edn-request-format)
13 | :response-format (edn-response-format)
14 | :handler (let [on-error-fn (or (args :on-error) identity)
15 | on-complete-fn (or (args :on-complete) identity)]
16 | (fn [[ok? data]]
17 | (if ok?
18 | (on-complete-fn data)
19 | (on-error-fn data)))))))
20 |
21 | (defn ajax-xhr
22 | [args]
23 | (ajax-request (assoc args
24 | :format (json-request-format)
25 | :response-format (json-response-format {:keywords? true})
26 | :handler (let [on-error-fn (or (args :on-error) identity)
27 | on-complete-fn (or (args :on-complete) identity)]
28 | (fn [[ok? data]]
29 | (if ok?
30 | (on-complete-fn data)
31 | (on-error-fn data)))))))
32 |
--------------------------------------------------------------------------------
/src/braid/page_inbox/commands.clj:
--------------------------------------------------------------------------------
1 | (ns braid.page-inbox.commands
2 | (:require
3 | [braid.core.server.db :as db]
4 | [braid.base.server.socket :as socket]
5 | [braid.chat.predicates :as p]
6 | [braid.chat.db.thread :as db.thread]))
7 |
8 | (def commands
9 | [{:id :braid.inbox/hide-thread!
10 | :params {:thread-id uuid?
11 | :user-id uuid?}
12 | :conditions
13 | (fn [{:keys [user-id thread-id]}]
14 | [[#(p/user-exists? (db/db) user-id)
15 | :not-found "User does not exist"]
16 | [#(p/thread-exists? (db/db) thread-id)
17 | :not-found "Thread does not exist"]
18 | [#(p/user-has-thread-open? (db/db) user-id thread-id)
19 | :forbidden "User does not have thread open"]])
20 | :effect
21 | (fn [{:keys [user-id thread-id]}]
22 | (db/run-txns!
23 | (db.thread/user-hide-thread-txn user-id thread-id))
24 | (socket/chsk-send! user-id [:braid.client/hide-thread thread-id]))}
25 |
26 | {:id :braid.inbox/show-thread!
27 | :params {:thread-id uuid?
28 | :user-id uuid?}
29 | :conditions
30 | (fn [{:keys [user-id thread-id]}]
31 | [[#(p/user-exists? (db/db) user-id)
32 | :not-found "User does not exist"]
33 | [#(p/thread-exists? (db/db) thread-id)
34 | :not-found "Thread does not exist"]
35 | [#(not (p/user-has-thread-open? (db/db) user-id thread-id))
36 | :forbidden "User already has thread open"]
37 | [#(p/user-can-access-thread? (db/db) user-id thread-id)
38 | :forbidden "User cannot access this thread"]])
39 | :effect
40 | (fn [{:keys [user-id thread-id]}]
41 | (db/run-txns! (db.thread/user-show-thread-txn user-id thread-id))
42 | (socket/chsk-send! user-id [:braid.client/show-thread
43 | (db.thread/thread-by-id thread-id)]))}])
44 |
--------------------------------------------------------------------------------
/src/braid/page_inbox/styles.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.page-inbox.styles
2 | (:require
3 | [braid.core.client.ui.styles.threads :refer [>threads]]
4 | [braid.core.client.ui.styles.mixins :as mixins]))
5 |
6 | (defn >new-thread []
7 | [:>.new-thread
8 | {;; background is set inline to group-color
9 | :border-radius "50%"
10 | :border "none"
11 | :flex-shrink 0
12 | :color "white"
13 | :font-size "4em"
14 | :width "5rem"
15 | :height "5rem"
16 | :cursor "pointer"}
17 |
18 | [:&:hover
19 | ;; double invert *trick* keeps the + white
20 | {:filter "invert(100%) brightness(1.8) invert(100%)"}]])
21 |
22 | (defn >inbox []
23 | [:>.inbox
24 | (>threads)
25 |
26 | [:>.threads
27 | [:>.sidebar
28 | {:display "flex"
29 | :flex-direction "column"
30 | :align-items "center"
31 | ;; instead of margins;
32 | ;; to allow for button to appear without nudging layout
33 | :width "9rem"
34 | :margin "0 0 1rem 0"}
35 |
36 | [:>button.resort-inbox
37 | (mixins/outline-button
38 | {:text-color "#aaa"
39 | :border-color "#ccc"
40 | :hover-text-color "#999"
41 | :hover-border-color "aaa"})
42 | {:margin-bottom "1em"}]
43 |
44 | (>new-thread)]]])
45 |
46 | (def styles
47 | [:>.page.inbox
48 | (>inbox)
49 |
50 | [:>.intro
51 |
52 | [:>button.clear-inbox
53 | (mixins/outline-button
54 | {:text-color "#aaa"
55 | :border-color "#ccc"
56 | :hover-text-color "#999"
57 | :hover-border-color "aaa"
58 | :icon \uf0d0})]]])
59 |
--------------------------------------------------------------------------------
/src/braid/page_subscriptions/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.page-subscriptions.core
2 | "Page for user to manage tag subscriptions"
3 | (:require
4 | [braid.base.api :as base]
5 | [braid.chat.api :as chat]
6 | #?@(:cljs
7 | [[braid.core.client.routes :as routes]
8 | [braid.page-subscriptions.styles :as styles]
9 | [braid.page-subscriptions.ui :as ui]])))
10 |
11 | (defn init! []
12 | #?(:cljs
13 | (do
14 | (chat/register-group-page!
15 | {:key :tags
16 | :view ui/tags-page-view})
17 |
18 | (chat/register-user-header-menu-item!
19 | {:body "Manage Subscriptions"
20 | :route-fn routes/group-page-path
21 | :route-args {:page-id "tags"}
22 | :icon \uf02c
23 | :priority 10})
24 |
25 | (base/register-styles!
26 | [:.app>.main
27 | styles/tags-page]))))
28 |
--------------------------------------------------------------------------------
/src/braid/page_subscriptions/styles.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.page-subscriptions.styles
2 | (:require
3 | [braid.core.client.ui.styles.mixins :as mixins]
4 | [braid.core.client.ui.styles.vars :as vars]
5 | [garden.units :refer [px rem em]]))
6 |
7 | (def tags-page
8 | [:>.page.tags
9 | [:>.title
10 | {:font-size "large"}]
11 | [:>.content
12 | (mixins/settings-container-style)
13 | [:>.new-tag
14 | (mixins/settings-item-style)
15 | [:input.error
16 | {:color "red"}]]
17 | [:>.tag-list
18 | (mixins/settings-item-style)
19 | [:>.tags
20 | {:margin-top (em 1)
21 | :color vars/grey-text}
22 |
23 | [:>.tag-info
24 | {:margin-bottom (em 1)}
25 |
26 | [:.description-edit
27 | [:textarea
28 | {:display "block"
29 | :width "100%"}]]
30 |
31 | [:>.button
32 | mixins/pill-button
33 | {:margin-left (em 1)}]
34 |
35 | [:button
36 | {:margin-right (rem 0.5)}
37 | [:&.delete
38 | {:color "red"}
39 | (mixins/fontawesome nil)]]
40 |
41 | [:>.count
42 | {:margin-right (em 0.5)}
43 |
44 | [:&::after
45 | {:margin-left (em 0.25)}]
46 |
47 | [:&.threads-count
48 | [:&::after
49 | (mixins/fontawesome \uf181)]]
50 |
51 | [:&.subscribers-count
52 | [:&::after
53 | (mixins/fontawesome \uf0c0)]]]]]]]])
54 |
--------------------------------------------------------------------------------
/src/braid/page_uploads/views/uploads_page_styles.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.page-uploads.views.uploads-page-styles
2 | (:require
3 | [garden.units :refer [rem em]]
4 | [braid.core.client.ui.styles.mixins :as mixins]
5 | [braid.core.client.ui.styles.vars :as vars]))
6 |
7 | (defn >uploads-page []
8 | [:>.page.uploads
9 |
10 | [:>.content
11 |
12 | [:button
13 | (mixins/settings-button)]
14 |
15 | [:>table.uploads
16 | {:width "100%"
17 | :flex-direction "row"
18 | :flex-wrap "wrap"
19 | :align-content "space-between"
20 | :align-item "bottom"}
21 |
22 | [:>tbody
23 |
24 | [:>tr.upload
25 |
26 | ["&:nth-child(odd)"
27 | {:background-color "#f9f9f9"}]
28 |
29 | ["&:nth-child(even)"
30 | {:background-color "white"}]
31 |
32 | [:>td.delete
33 |
34 | [:>button
35 | {:color "red"}
36 | (mixins/fontawesome nil)]]
37 |
38 | [:>td.uploaded-file
39 |
40 | [:img :video
41 | {:width (rem 5)
42 | :margin 0}]
43 |
44 | [:>td.uploaded-thread
45 | {:height (rem 3)
46 | :margin (em 1)}
47 |
48 | [:>.tags
49 | {:display "inline"}]]]]]]]])
50 |
--------------------------------------------------------------------------------
/src/braid/permalinks/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.permalinks.core
2 | "Includes a permalink in thread headers that leads to a dedicated page"
3 | (:require
4 | [braid.chat.api :as chat]
5 | #?@(:cljs
6 | [[re-frame.core :refer [subscribe dispatch]]
7 | [braid.core.client.routes :as routes]
8 | [braid.core.client.ui.views.threads :refer [threads-view]]])))
9 |
10 | (defn init! []
11 | #?(:cljs
12 | (do
13 | (chat/register-thread-control!
14 | {:priority -1
15 | :view
16 | (fn [thread]
17 | [:a.control.permalink
18 | {:title "Go to Permalink"
19 | :href (routes/group-page-path
20 | {:query-params {:thread-id (thread :id)}
21 | :page-id "thread"
22 | :group-id (thread :group-id)})}
23 | \uf0c1])})
24 |
25 | (chat/register-group-page!
26 | {:key :thread
27 | :on-load (fn [page]
28 | (dispatch [:load-threads! {:thread-ids [(uuid (page :thread-id))]}]))
29 | :view
30 | (fn []
31 | [:div.page
32 | [:div.intro
33 | [:div.title "Thread"]]
34 | (let [page @(subscribe [:page])]
35 | (if-let [thread @(subscribe [:thread (uuid (page :thread-id))])]
36 | [threads-view {:threads [thread]}]
37 | [:div.loading]))])}))))
38 |
39 |
--------------------------------------------------------------------------------
/src/braid/popovers/api.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.popovers.api
2 | (:require
3 | [braid.base.api :as base]))
4 |
5 | (defn register-popover-styles! [styles]
6 | (base/register-styles!
7 | [:#app>.app>.main
8 | [:>.popover
9 | styles]]))
10 |
--------------------------------------------------------------------------------
/src/braid/popovers/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.popovers.core
2 | "Allows modules to render popovers.
3 |
4 | Register styles with:
5 | braid.popovers.register-popover-styles!
6 |
7 | Trigger popovers by:
8 | {:on-mouse-enter
9 | (braid.popovers.helpers/on-mouse-enter
10 | (fn []
11 | [view-to-show]))}"
12 | (:require
13 | [braid.base.api :as base]
14 | #?@(:cljs
15 | [[braid.popovers.impl :as impl]])))
16 |
17 | (defn init! []
18 | #?(:cljs
19 | (do
20 | (base/register-root-view! impl/view))))
21 |
--------------------------------------------------------------------------------
/src/braid/popovers/helpers.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.popovers.helpers
2 | (:require
3 | [braid.popovers.impl :as impl]))
4 |
5 | (defn on-mouse-enter [view]
6 | (fn [e]
7 | (reset! impl/popover
8 | {:target (.-currentTarget e)
9 | :view view})))
10 |
11 | (defn on-touch-start
12 | [view]
13 | (fn [e]
14 | (reset! impl/popover {:target (.-currentTarget e)
15 | :view view})))
16 |
17 | (defn on-click
18 | [view]
19 | (fn [e]
20 | (reset! impl/popover {:target (.-currentTarget e)
21 | :view view
22 | :modal? true})))
23 |
24 | (defn close!
25 | []
26 | (reset! impl/popover nil))
27 |
--------------------------------------------------------------------------------
/src/braid/popovers/impl.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.popovers.impl
2 | (:require
3 | [reagent.core :as r]))
4 |
5 | (def popover (r/atom nil))
6 |
7 | (defn view []
8 | (when-let [{:keys [view target modal?]} @popover]
9 | (let [bounds (if modal?
10 | #js {:width js/document.documentElement.clientWidth
11 | :height js/document.documentElement.clientHeight
12 | :left js/document.documentElement.clientLeft
13 | :top js/document.documentElement.clientTop}
14 | (.getBoundingClientRect target))]
15 | [:div.popover
16 | {:on-mouse-leave (fn []
17 | (when-not modal?
18 | (reset! popover nil)))
19 | :on-click (fn [e]
20 | (when (= (.-target e) (.-currentTarget e))
21 | (reset! popover nil)))
22 | :on-touch-start (fn [e]
23 | (when (= (.-target e) (.-currentTarget e))
24 | (reset! popover nil)))
25 | :style
26 | (merge {:position "absolute"
27 | :z-index 10000
28 | :width (.-width bounds)
29 | :height (.-height bounds)
30 | :top (+ (.-scrollY js/window) (.-top bounds))
31 | :left (+ (.-scrollX js/window) (.-left bounds))}
32 | (when modal?
33 | {:background-color "rgba(0, 0, 0, 0.5)"
34 | :display "flex"
35 | :justify-content "center"
36 | :align-items "center"}))}
37 |
38 | [view]])))
39 |
--------------------------------------------------------------------------------
/src/braid/quests/client/core.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.quests.client.core
2 | (:require
3 | [clojure.spec.alpha :as s]
4 | [re-frame.core :as re-frame]
5 | [braid.quests.client.helpers :as helpers]
6 | [braid.quests.client.remote-handlers]))
7 |
8 | (defn initial-data-handler
9 | [db data]
10 | (helpers/set-quest-records db (data :quest-records)))
11 |
12 | (def event-listener
13 | (fn [event]
14 | (when (not= "quests" (namespace (first event)))
15 | (re-frame/dispatch [:quests/update-handler! event]))))
16 |
17 | (def initial-state {::quest-records {}})
18 | (def schema {::quest-records
19 | {uuid?
20 | {:quest-record/id uuid?
21 | :quest-record/quest-id keyword?
22 | :quest-record/progress integer?
23 | :quest-record/state (s/spec #{:inactive
24 | :active
25 | :complete
26 | :skipped})}}})
27 |
--------------------------------------------------------------------------------
/src/braid/quests/client/helpers.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.quests.client.helpers
2 | (:require
3 | [braid.core.client.state.helpers :refer [key-by-id key-by]]
4 | [braid.quests.client.list :refer [quests]]))
5 |
6 | ; getters
7 |
8 | (defn get-active-quest-records [state]
9 | (->> state
10 | :braid.quests.client.core/quest-records
11 | vals
12 | (filter (fn [quest-record]
13 | (= (quest-record :quest-record/state) :active)))
14 | (sort-by :quest/order)))
15 |
16 | (defn get-next-quest [state]
17 | (let [quest-ids-with-records (->> state
18 | :braid.quests.client.core/quest-records
19 | vals
20 | (map :quest-record/quest-id)
21 | set)
22 | next-quest (->> quests
23 | (sort-by :quest/order)
24 | (remove (fn [quest]
25 | (contains? quest-ids-with-records (quest :quest/id))))
26 | first)]
27 | next-quest))
28 |
29 | (defn get-quest-record [state quest-record-id]
30 | (get-in state [:braid.quests.client.core/quest-records quest-record-id]))
31 |
32 | ; setters
33 |
34 | (defn set-quest-records [state quest-records]
35 | (assoc-in state [:braid.quests.client.core/quest-records] (key-by :quest-record/id quest-records)))
36 |
37 | (defn store-quest-record [state quest-record]
38 | (assoc-in state [:braid.quests.client.core/quest-records (quest-record :quest-record/id)] quest-record))
39 |
40 | (defn complete-quest [state quest-record-id]
41 | (assoc-in state [:braid.quests.client.core/quest-records quest-record-id :quest-record/state] :complete))
42 |
43 | (defn skip-quest [state quest-record-id]
44 | (assoc-in state [:braid.quests.client.core/quest-records quest-record-id :quest-record/state] :skipped))
45 |
46 | (defn increment-quest [state quest-record-id]
47 | (update-in state [:braid.quests.client.core/quest-records quest-record-id :quest-record/progress] inc))
48 |
--------------------------------------------------------------------------------
/src/braid/quests/client/remote_handlers.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.quests.client.remote-handlers
2 | (:require
3 | [braid.base.client.socket :as socket] ;; FIXME should register! not use directly
4 | [re-frame.core :refer [dispatch]]))
5 |
6 | (defmethod socket/event-handler :braid.quests/upsert-quest-record
7 | [[_ quest-record]]
8 | (dispatch [:quests/upsert-quest-record! quest-record]))
9 |
--------------------------------------------------------------------------------
/src/braid/quests/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.quests.core
2 | "Provides 'quests' for Braid - activies that users can complete to introduce them to the system"
3 | (:require
4 | [braid.base.api :as base]
5 | [braid.chat.api :as chat]
6 | #?@(:cljs
7 | [[braid.quests.client.views :refer [quests-header-view]]
8 | [braid.quests.client.styles :refer [quests-header]]
9 | [braid.quests.client.events :refer [events]]
10 | [braid.quests.client.helpers :as helpers]
11 | [braid.quests.client.core :refer [initial-data-handler
12 | event-listener
13 | initial-state
14 | schema]]]
15 | :clj
16 | [[braid.quests.server.core :refer [db-schema
17 | initial-user-data-fn
18 | server-message-handlers]]
19 | [braid.quests.server.db :refer [activate-first-quests-txn]]])))
20 |
21 | (defn init! []
22 | #?(:cljs
23 | (do
24 | (chat/register-header-view! quests-header-view)
25 | (base/register-styles! quests-header)
26 | (base/register-initial-user-data-handler! initial-data-handler)
27 | (base/register-event-listener! event-listener)
28 | (base/register-state! initial-state schema)
29 | (base/register-events! events)
30 | (base/register-subs!
31 | {:quests/active-quest-records
32 | (fn [state _]
33 | (helpers/get-active-quest-records state))}))
34 |
35 | :clj
36 | (do
37 | (base/register-db-schema! db-schema)
38 | (base/register-initial-user-data! initial-user-data-fn)
39 | (base/register-server-message-handlers! server-message-handlers)
40 | (chat/register-post-create-user-txn! activate-first-quests-txn))))
41 |
--------------------------------------------------------------------------------
/src/braid/quests/server/core.clj:
--------------------------------------------------------------------------------
1 | (ns braid.quests.server.core
2 | (:require
3 | [datomic.db] ; for reader macro
4 | [braid.quests.server.db :as db]))
5 |
6 | (def db-schema
7 | [{:db/ident :quest-record/id
8 | :db/valueType :db.type/uuid
9 | :db/cardinality :db.cardinality/one
10 | :db/unique :db.unique/identity}
11 | {:db/ident :quest-record/quest-id
12 | :db/valueType :db.type/keyword
13 | :db/cardinality :db.cardinality/one}
14 | {:db/ident :quest-record/user
15 | :db/valueType :db.type/ref
16 | :db/cardinality :db.cardinality/one}
17 | {:db/ident :quest-record/state
18 | :db/valueType :db.type/keyword
19 | :db/cardinality :db.cardinality/one}
20 | {:db/ident :quest-record/progress
21 | :db/valueType :db.type/long
22 | :db/cardinality :db.cardinality/one}])
23 |
24 | (defn initial-user-data-fn
25 | [user-id]
26 | {:quest-records (db/get-active-quests-for-user-id user-id)})
27 |
28 | (def server-message-handlers
29 | {:braid.server.quests/upsert-quest-record
30 | (fn [{:keys [?data user-id]}]
31 | {:chsk-send! [user-id [:braid.quests/upsert-quest-record ?data]]
32 | :db-run-txns! (db/upsert-quest-record-txn user-id ?data)})})
33 |
--------------------------------------------------------------------------------
/src/braid/quests/server/db.clj:
--------------------------------------------------------------------------------
1 | (ns braid.quests.server.db
2 | (:require
3 | [braid.core.server.db :as db]
4 | [datomic.api :as d]))
5 |
6 | ; Pull Patterns
7 |
8 | (def quest-record-pull-pattern
9 | [:quest-record/id
10 | :quest-record/quest-id
11 | :quest-record/progress
12 | :quest-record/state])
13 |
14 | ; Queries
15 |
16 | (defn get-active-quests-for-user-id
17 | [user-id]
18 | (->> (d/q '[:find (pull ?qr pull-pattern)
19 | :in $ ?user-id pull-pattern
20 | :where
21 | [?qr :quest-record/user ?u]
22 | [?u :user/id ?user-id]
23 | [?qr :quest-record/state :active]]
24 | (db/db)
25 | user-id
26 | quest-record-pull-pattern)
27 | (map first)))
28 |
29 | ; Transactions
30 |
31 | (defn upsert-quest-record-txn
32 | [user-id quest-record]
33 | (let [db-id (if (d/entity (db/db) [:quest-record/id (quest-record :quest-record/id)])
34 | [:quest-record/id (quest-record :quest-record/id)]
35 | (d/tempid :entities))]
36 | [(assoc quest-record
37 | :db/id db-id
38 | :quest-record/user [:user/id user-id])]))
39 |
40 | (defn activate-first-quests-txn
41 | [user-id]
42 | (->> [:quest/quest-complete :quest/conversation-new :quest/conversation-reply]
43 | (map (fn [quest-id]
44 | {:db/id (d/tempid :entities)
45 | :quest-record/id (d/squuid)
46 | :quest-record/user user-id
47 | :quest-record/quest-id quest-id
48 | :quest-record/progress 0
49 | :quest-record/state :active}))))
50 |
--------------------------------------------------------------------------------
/src/braid/search/client.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.search.client
2 | (:require
3 | [braid.core.hooks :as hooks]))
4 |
5 | (defonce search-results-views
6 | (hooks/register! (atom {}) {keyword? {:view fn?
7 | :styles vector?
8 | :priority number?}}))
9 |
--------------------------------------------------------------------------------
/src/braid/search/server.clj:
--------------------------------------------------------------------------------
1 | (ns braid.search.server
2 | (:require
3 | [clojure.set :as set]
4 | [braid.core.hooks :as hooks]))
5 |
6 | (defonce search-functions
7 | (hooks/register! (atom []) [fn?]))
8 |
9 | (defonce search-type-auth-check
10 | (hooks/register! (atom {}) {keyword? fn?}))
11 |
12 | (defn- identity-filter
13 | [_ results]
14 | results)
15 |
16 | (defn search-as
17 | "Return the ids of all threads, visible to the user, in the given group,
18 | matching the provided query.
19 | The query can specify tags by prefixing them with an octothorpe; for example,
20 | the query 'foo #bar' will find any threads tagged with 'bar' containing the
21 | text 'foo'"
22 | [{:keys [user-id query group-id]}]
23 | ; TODO: pagination?
24 | (let [results (pmap (fn [search-fn]
25 | (search-fn {:user-id user-id
26 | :query query
27 | :group-id group-id}))
28 | @search-functions)]
29 | (->> (remove nil? results)
30 | ;; group the results by type, without merging all the results
31 | ;; together, so we can take the intersection of the multiple
32 | ;; search functions for a given type
33 | (reduce
34 | (fn [m set-of-maps]
35 | (->> (group-by :search/type set-of-maps)
36 | (reduce (fn [m [type results]]
37 | (update m type (fnil conj []) (set results)))
38 | m)))
39 | {})
40 | (reduce
41 | (fn [m [type results]]
42 | (assoc m type ((get @search-type-auth-check type identity-filter)
43 | user-id
44 | (apply set/intersection results))))
45 | {}))))
46 |
--------------------------------------------------------------------------------
/src/braid/search/tags.clj:
--------------------------------------------------------------------------------
1 | (ns braid.search.tags
2 | (:require
3 | [clojure.string :as string]
4 | [datomic.api :as d]
5 | [braid.core.server.db :as db]))
6 |
7 | (defn search-tags-by-name
8 | [{:keys [query group-id]}]
9 | (when-let [tags (->> query (re-seq #"[#]([^ ]+)") (map second) seq)]
10 | (->> (d/q '[:find ?t-id ?tag-name
11 | :in $ [?tag-name ...] ?g-id
12 | :where
13 | [?g :group/id ?g-id]
14 | [?t :tag/name ?tag-name-case]
15 | [(clojure.string/lower-case ?tag-name-case) ?tag-name]
16 | [?t :tag/group ?g]
17 | [?t :tag/id ?t-id]]
18 | (db/db)
19 | (map string/lower-case tags)
20 | group-id)
21 | (into #{}
22 | (map (fn [[tag-id tag-name]]
23 | {:search/type :tag
24 | :search/sort-key tag-name
25 | :tag-id tag-id}))))))
26 |
--------------------------------------------------------------------------------
/src/braid/search/ui/search_bar.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.search.ui.search-bar
2 | (:require
3 | [reagent.core :as r]
4 | [re-frame.core :refer [subscribe dispatch]]
5 | [braid.core.client.routes :as routes]
6 | [braid.lib.color :as color]))
7 |
8 | (defn search-bar-view
9 | []
10 | (r/with-let [search-query (r/atom @(subscribe [:braid.search/query]))
11 | prev-page (r/atom (:type @(subscribe [:page])))]
12 | (let [current-page (:type @(subscribe [:page]))]
13 | (when (not= @prev-page current-page)
14 | (if (= @prev-page :search)
15 | (reset! search-query "")
16 | (reset! search-query @(subscribe [:braid.search/query])))
17 | (reset! prev-page current-page)))
18 | [:div.search-bar
19 | [:input {:type "text"
20 | :placeholder "Search..."
21 | :value @search-query
22 | :on-change
23 | (fn [e]
24 | (reset! search-query (.. e -target -value))
25 | (dispatch [:braid.search/update-query! (.. e -target -value)]))}]
26 | (if (and @search-query (not= "" @search-query))
27 | [:a.action.clear
28 | {:on-click (fn [] (reset! search-query ""))
29 | :href (routes/group-page-path {:group-id @(subscribe [:open-group-id])
30 | :page-id "inbox"})
31 | :style {:color (color/->color @(subscribe [:open-group-id]))}}]
32 | [:div.action.search])]))
33 |
--------------------------------------------------------------------------------
/src/braid/search/ui/search_button.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.search.ui.search-button
2 | (:require
3 | [re-frame.core :refer [subscribe]]
4 | [braid.core.client.routes :as routes]))
5 |
6 | (defn search-button-view [query]
7 | [:a.search
8 | {:href (routes/group-page-path {:group-id @(subscribe [:open-group-id])
9 | :page-id "search"
10 | :query-params {:query query}})}
11 | "Search"])
12 |
--------------------------------------------------------------------------------
/src/braid/search/ui/search_page_styles.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.search.ui.search-page-styles
2 | (:require
3 | [braid.core.client.ui.styles.vars :as vars]))
4 |
5 | (def >search-page
6 | [:>.page.search
7 | [:>.title
8 | {:position "absolute"
9 | :top "0"}]
10 | [:>.search-results
11 | {:position "absolute"
12 | :bottom 0
13 | :display "flex"
14 | :height "100%"
15 | :flex-direction "row"
16 | :align-items "flex-end"}
17 | [:>.result
18 | {:position "relative"
19 | :min-width "min-content"
20 | :display "flex"
21 | :align-items "flex-end"
22 | :height "90%"}
23 | [:>.description
24 | {:position "absolute"
25 | :color vars/grey-text
26 | :top "1rem"
27 | :left "1rem"}]]]])
28 |
--------------------------------------------------------------------------------
/src/braid/search/ui/tag_results.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.search.ui.tag-results
2 | (:require
3 | [braid.core.client.ui.views.tag-hover-card :as tag-card]
4 | [braid.core.client.ui.styles.hover-cards :as card-styles]))
5 |
6 | (defn search-tags-view
7 | [_ tags]
8 | [:div.result.tag
9 | [:div.description
10 | (str (count tags) " tags" (when (not= (count tags) 1) "s"))]
11 | [:div.tags.content
12 | (doall
13 | (for [{:keys [tag-id]} tags]
14 | ^{:key tag-id}
15 | [tag-card/tag-hover-card-view tag-id]))]])
16 |
17 | (def styles
18 | [:>.result.tag
19 | [:>.tags.content
20 | ;; would like to just do "flex-wrap: wrap" but apparently a
21 | ;; flex-direction: column + flex-wrap: wrap doesn't grow the width
22 | ;; of the parent when it wraps, so then this ends up overlapping
23 | ;; with the search results next to it
24 | {:display "flex"
25 | :flex-direction "column"
26 | :align-items "flex-end"
27 | :max-height "90%"
28 | :gap "0.5rem"
29 | :margin-left "0.5rem"
30 | :overflow-y "auto"
31 | :padding "0.25rem"}
32 | card-styles/>tag-card
33 | [:>.card.tag
34 | {:min-height "3rem"
35 | :margin-top 0
36 | :margin-left "0.5rem"}]]])
37 |
--------------------------------------------------------------------------------
/src/braid/search/ui/user_results.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.search.ui.user-results
2 | (:require
3 | [braid.core.client.ui.views.user-hover-card :as user-card]
4 | [braid.core.client.ui.styles.hover-cards :as card-styles]))
5 |
6 | (defn search-users-view
7 | [_ users]
8 | [:div.result.user
9 | [:div.description
10 | (str (count users) " user" (when (not= (count users) 1) "s"))]
11 | [:div.users.content
12 | (doall
13 | (for [{:keys [user-id]} users]
14 | ^{:key user-id}
15 | [user-card/user-hover-card-view user-id]))]])
16 |
17 | (def styles
18 | [:>.result.user
19 | [:>.users.content
20 | ;; would like to just do "flex-wrap: wrap" but apparently a
21 | ;; flex-direction: column + flex-wrap: wrap doesn't grow the width
22 | ;; of the parent when it wraps, so then this ends up overlapping
23 | ;; with the search results next to it
24 | {:display "flex"
25 | :flex-direction "column"
26 | :align-items "flex-end"
27 | :max-height "90%"
28 | :gap "0.5rem"
29 | :margin-left "0.5rem"
30 | :overflow-y "auto"
31 | :padding "0.25rem"}
32 | card-styles/>user-card
33 | [:>.card.user
34 | {:min-height "5rem"
35 | :margin-top 0
36 | :margin-left "0.5rem"}]]])
37 |
--------------------------------------------------------------------------------
/src/braid/search/users.clj:
--------------------------------------------------------------------------------
1 | (ns braid.search.users
2 | (:require
3 | [datomic.api :as d]
4 | [braid.core.server.db :as db]))
5 |
6 | (defn search-users-by-name
7 | [{:keys [query group-id]}]
8 | (when-let [users (->> query (re-seq #"[@]([^ ]+)") (map second) seq)]
9 | (->> (d/q '[:find ?u-id ?user-name
10 | :in $ [?user-name ...] ?g-id
11 | :where
12 | [?g :group/id ?g-id]
13 | [?g :group/user ?user]
14 | [?user :user/nickname ?user-name]
15 | [?user :user/id ?u-id]]
16 | (db/db)
17 | users
18 | group-id)
19 | (into #{}
20 | (map (fn [[user-id user-name]]
21 | {:search/type :user
22 | :search/sort-key user-name
23 | :user-id user-id}))))))
24 |
--------------------------------------------------------------------------------
/src/braid/sidebar/api.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.sidebar.api
2 | (:require
3 | [braid.sidebar.ui :as ui]))
4 |
5 | ;; TODO make it so that it can also add styles
6 | (defn register-button!
7 | [view]
8 | (swap! ui/sidebar-extra-views conj view))
9 |
--------------------------------------------------------------------------------
/src/braid/sidebar/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.sidebar.core
2 | (:require
3 | [braid.base.api :as base]
4 | #?@(:cljs
5 | [[braid.sidebar.styles :as styles]
6 | [braid.sidebar.ui :as ui]])))
7 |
8 | (defn init! []
9 | #?(:cljs
10 | (do
11 | (base/register-styles!
12 | [:.main
13 | styles/>sidebar])
14 |
15 | (base/register-root-view!
16 | ui/sidebar-view))))
17 |
--------------------------------------------------------------------------------
/src/braid/uploads/db.clj:
--------------------------------------------------------------------------------
1 | (ns braid.uploads.db
2 | (:require
3 | [datomic.api :as d]
4 | [braid.core.server.db :as db]
5 | [braid.chat.db.common :refer [create-entity-txn]]))
6 |
7 | (def upload-pull-pattern
8 | [:upload/id
9 | :upload/url
10 | :upload/uploaded-at
11 | {:upload/thread [:thread/id]}
12 | {:upload/uploaded-by [:user/id]}])
13 |
14 | (defn db->upload [e]
15 | {:id (:upload/id e)
16 | :uploaded-at (:upload/uploaded-at e)
17 | :thread-id (get-in e [:upload/thread :thread/id])
18 | :uploader-id (get-in e [:upload/uploaded-by :user/id])
19 | :url (:upload/url e)})
20 |
21 | ;; Queries
22 |
23 | (defn uploads-in-group
24 | [group-id]
25 | (->> (d/q '[:find (pull ?u pull-pattern)
26 | :in $ ?group-id pull-pattern
27 | :where
28 | [?g :group/id ?group-id]
29 | [?t :thread/group ?g]
30 | [?u :upload/thread ?t]]
31 | (db/db) group-id upload-pull-pattern)
32 | (map (comp db->upload first))
33 | (sort-by :uploaded-at #(compare %2 %1))))
34 |
35 | (defn upload-info
36 | [upload-id]
37 | (->> (d/pull (db/db)
38 | [{:upload/uploaded-by [:user/id]}
39 | {:upload/thread [{:thread/group [:group/id]}]}
40 | :upload/url]
41 | [:upload/id upload-id])
42 | ((fn [up]
43 | {:id upload-id
44 | :group-id (get-in up [:upload/thread :thread/group :group/id])
45 | :user-id (get-in up [:upload/uploaded-by :user/id])
46 | :url (:upload/url up)}))))
47 |
48 | ;; Transactions
49 |
50 | (defn create-upload-txn
51 | [{:keys [id url thread-id group-id uploader-id uploaded-at]}]
52 | (let [thread (d/tempid :entities)]
53 | (concat
54 | [{:db/id thread
55 | :thread/id thread-id
56 | :thread/group [:group/id group-id]}]
57 | [[:db/add [:user/id uploader-id] :user/subscribed-thread thread]]
58 | (create-entity-txn
59 | {:upload/id id
60 | :upload/url url
61 | :upload/thread thread
62 | :upload/uploaded-by [:user/id uploader-id]
63 | :upload/uploaded-at uploaded-at}
64 | db->upload))))
65 |
66 | (defn retract-upload-txn
67 | [upload-id]
68 | [[:db.fn/retractEntity [:upload/id upload-id]]])
69 |
--------------------------------------------------------------------------------
/src/braid/users/client/views/users_page.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.users.client.views.users-page
2 | (:require
3 | [re-frame.core :refer [dispatch subscribe]]))
4 |
5 | (defn user-view
6 | [{:keys [nickname id] :as user}]
7 | (let [group-id (subscribe [:open-group-id])]
8 | [:tr.user
9 | [:td.nickname nickname]
10 | [:td.action
11 | (if @(subscribe [:user-is-group-admin? id] [group-id])
12 | [:span.admin "Admin"]
13 | [:button.make-admin
14 | {:on-click (fn [_]
15 | (dispatch [:make-admin!
16 | {:group-id @group-id
17 | :user-id id}]))}
18 | "Make Admin"])
19 | (when (not= id @(subscribe [:user-id]))
20 | [:button.ban {:on-click (fn []
21 | (dispatch [:remove-from-group!
22 | {:group-id @group-id
23 | :user-id id}]))}
24 | "Kick from Group"])]]))
25 |
26 | (defn users-page-view []
27 | (when @(subscribe [:current-user-is-group-admin?]
28 | [(subscribe [:open-group-id])])
29 | (let [users @(subscribe [:users])]
30 | [:div.page.users
31 | [:div.title "Users"]
32 | [:div.content
33 | (cond
34 | (nil? users)
35 | [:p.users "Loading..."]
36 |
37 | (empty? users)
38 | [:p.users "No users yet"]
39 |
40 | :else
41 | [:table.users
42 | [:thead
43 | [:tr
44 | [:th "Nickname"]
45 | [:th "Actions"]]]
46 | [:tbody
47 | (doall
48 | (for [user users]
49 | ^{:key (user :id)}
50 | [user-view user]))]])]])))
51 |
--------------------------------------------------------------------------------
/src/braid/users/client/views/users_page_styles.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.users.client.views.users-page-styles
2 | (:require
3 | [braid.core.client.ui.styles.mixins :as mixins]
4 | [braid.core.client.ui.styles.vars :as vars]
5 | [garden.units :refer [rem em px]]))
6 |
7 | (def users-page
8 | [:.app>.main>.page.users
9 | [:>.title
10 | {:font-size "large"}]
11 |
12 | [:>.content
13 | (mixins/settings-container-style)
14 |
15 | [:>.users
16 | (mixins/settings-item-style)]]])
17 |
--------------------------------------------------------------------------------
/src/braid/users/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.users.core
2 | (:require
3 | [braid.base.api :as base]
4 | [braid.chat.api :as chat]
5 | #?@(:cljs
6 | [[braid.users.client.views.users-page :as views]
7 | [braid.users.client.views.users-page-styles :as styles]
8 | [braid.core.client.routes :as routes]])))
9 |
10 | (defn init! []
11 | #?(:cljs
12 | (do
13 | (chat/register-group-page!
14 | {:key :users
15 | :view views/users-page-view})
16 | (chat/register-admin-header-item!
17 | {:class "users"
18 | :route-fn routes/group-page-path
19 | :route-args {:page-id "users"}
20 | :icon \uf0c0
21 | :body "Users"})
22 | (base/register-styles! styles/users-page))))
23 |
--------------------------------------------------------------------------------
/src/braid/version/core.cljc:
--------------------------------------------------------------------------------
1 | (ns braid.version.core
2 | (:require
3 | [braid.base.api :as base]
4 | #?@(:clj
5 | [[braid.base.conf :refer [config]]])))
6 |
7 | (defn init! []
8 |
9 | #?(:clj
10 | (do
11 | (base/register-config-var! :version :optional [:string])
12 |
13 | (base/register-additional-script!
14 | {:body
15 | ;; can't just include a string here
16 | ;; b/c config is not ready at "compile" time
17 | ;; needs to be at run time
18 | (reify Object
19 | (toString [_]
20 | (str "window.version='" (config :version) "';")))}))
21 |
22 | :cljs
23 | (do
24 | (base/register-root-view!
25 | (fn []
26 | [:div.version
27 | js/window.version]))
28 |
29 | (base/register-styles!
30 | [:.main>.version
31 | {:opacity 0
32 | :position "absolute"
33 | :bottom 0
34 | :right 0}
35 |
36 | [:&:hover
37 | {:opacity 1}]]))))
38 |
--------------------------------------------------------------------------------
/test/braid/test/client/runners/doo.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.test.client.runners.doo
2 | "Test runner that runs tests using doo"
3 | (:require
4 | [braid.test.client.runners.tests]
5 | [doo.runner :refer-macros [doo-all-tests]]))
6 |
7 | (doo-all-tests #"(braid\.test\.client)\..*-test")
8 |
--------------------------------------------------------------------------------
/test/braid/test/client/runners/tests.cljs:
--------------------------------------------------------------------------------
1 | (ns braid.test.client.runners.tests
2 | "A namespace that acts as a proxy for test runners to require all
3 | client test namespaces. This is useful if we ever have multiple test
4 | runners (e.g. doo, devcards, etc). New test namespaces need to be
5 | required here to ensure they are loaded, otherwise test runners
6 | cannot find them."
7 | (:require
8 | [braid.test.client.ui.views.message-test]))
9 |
--------------------------------------------------------------------------------
/test/braid/test/fixtures/conf.clj:
--------------------------------------------------------------------------------
1 | (ns braid.test.fixtures.conf
2 | (:require
3 | [braid.core.modules :as modules]
4 | [braid.base.conf :as conf]
5 | [mount.core :as mount]))
6 |
7 | (defn start-config
8 | [t]
9 | (modules/init! modules/default)
10 | (-> (mount/only #{#'conf/config})
11 | (mount/with-args {:port 0})
12 | (mount/start))
13 | (t)
14 | (mount/stop))
15 |
--------------------------------------------------------------------------------
/test/braid/test/fixtures/db.clj:
--------------------------------------------------------------------------------
1 | (ns braid.test.fixtures.db
2 | (:require
3 | [braid.core.modules :as modules]
4 | [braid.base.conf :as conf]
5 | [braid.core.server.db :as db]
6 | [datomic.api]
7 | [mount.core :as mount]))
8 |
9 | (defn drop-db [t]
10 | (modules/init! modules/default)
11 | (-> (mount/only #{#'conf/config #'db/conn})
12 | (mount/with-args {:port 0
13 | :db-url "datomic:mem://chat-test"})
14 | (mount/start))
15 | (t)
16 | (datomic.api/delete-database (conf/config :db-url))
17 | (mount/stop))
18 |
--------------------------------------------------------------------------------
/test/braid/test/server/test_utils.clj:
--------------------------------------------------------------------------------
1 | (ns braid.test.server.test-utils
2 | (:require
3 | [datomic.api :as d]
4 | [braid.core.server.db :as db]))
5 |
6 | (defn- db->message
7 | [e]
8 | {:id (:message/id e)
9 | :content (:message/content e)
10 | :user-id (:user/id (:message/user e))
11 | :thread-id (:thread/id (:message/thread e))
12 | :group-id (get-in e [:message/thread :thread/group :group/id])
13 | :created-at (:message/created-at e)})
14 |
15 | (defn fetch-messages
16 | "Helper function to fetch all messages"
17 | []
18 | (->> (d/q '[:find (pull ?e
19 | [:message/id
20 | :message/content
21 | :message/created-at
22 | {:message/user [:user/id]}
23 | {:message/thread [:thread/id
24 | {:thread/group [:group/id]}]}])
25 | :where [?e :message/id]]
26 | (d/db db/conn))
27 | (map (comp db->message first))))
28 |
--------------------------------------------------------------------------------
/test/helpers/cookies.clj:
--------------------------------------------------------------------------------
1 | (ns helpers.cookies
2 | (:require
3 | [ring.util.codec :as codec]
4 | [clojure.string :as string])
5 | (:import
6 | (java.time Instant)
7 | (java.time.format DateTimeFormatter)))
8 |
9 | (let [parser DateTimeFormatter/RFC_1123_DATE_TIME]
10 | (defn- parse-rfc1123 [s]
11 | "Parse RFC822/RFC1123 date per http://tools.ietf.org/html/rfc2616#section-3.3.1"
12 | (.. parser (parseDateTime s) getMillis)))
13 |
14 | (let [matchers [#(when-let [match (re-find #"Max-Age=(\d+)" %)]
15 | [:max-age (.. Instant now (plusMillis (long (* 1000 (Integer/parseInt (match 1))))) toEpochMilli)])
16 | #(when-let [match (re-find #"Expires=(.+)" %)]
17 | [:expires (parse-rfc1123 (match 1))])
18 | #(when-let [match (re-find #"Domain=(.+)" %)]
19 | [:domain (match 1)])
20 | #(when-let [match (re-find #"Path=(.*)" %)]
21 | [:path (match 1)])
22 | #(when (= "Secure" %) [:secure true])
23 | #(when (= "HttpOnly" %) [:http-only true])]]
24 |
25 | (defn- parse-cookie-attrs [attr-string]
26 | "Parse optional cookie attributes per RFC 6265"
27 | (let [attrs (string/split attr-string #";")
28 | matches (map (fn [a] (some (fn [m] (m a)) matchers)) attrs)]
29 | ;; max-age dominates expires
30 | (reduce (fn [m [k v]] (if (= :max-age k) (merge {:expires v} m) (merge m {k v}))) {} matches))))
31 |
32 | (defn parse-cookie [c]
33 | "Parse cookies per http://tools.ietf.org/html/rfc6265"
34 | (let [x (re-find #"([^;]+)(?:;(.*))*" c)
35 | [name value] (first (codec/form-decode (x 1)))
36 | av-map (parse-cookie-attrs (x 2))]
37 | {name (merge av-map {:value value})}))
38 |
39 | (defn write-cookie [[name {value :value}]]
40 | (codec/form-encode {name value}))
41 |
42 | (defn write-cookies [cookies]
43 | (string/join ";" (map write-cookie cookies)))
44 |
--------------------------------------------------------------------------------
/test/integration/braid/system_start_stop_test.clj:
--------------------------------------------------------------------------------
1 | (ns integration.braid.system-start-stop-test
2 | (:require [braid.core :refer :all]
3 | [clojure.test :refer :all]))
4 |
5 | (deftest start-stop
6 | (is (start! 0))
7 | (is (stop!)))
8 |
--------------------------------------------------------------------------------