profile image
Sean Walker
2020-08-08

Let’s make twitter with joy: part 6

Welcome to a series I call with joy where I clone popular websites/webapps with my web framework, joy.

If you’re just tuning in here’s Part 5.

Let’s look at where we are on the roadmap:

So we’re coming to the end of this, thank goodness. Profiles and follow/unfollow. Let’s do it

Follow/Unfollow

I have this pretty down pat at this point, you have two routes:

  1. (route :post "/follows" :follows/create)
  2. (route :delete "/follows/:id" :follows/delete)

And you hook them up with htmx:

(route :post "/follows" :follows/create)
(route :delete "/follows/:id" :follows/delete)

(defn follow-button [followed &opt follow]
  [:form
    [:input {:type "hidden" :name "followed-id" :value (get followed :id)}]
    [:a (merge {:href "#" :hx-swap "outerHTML"}
               (if follow
                 {:hx-delete (url-for :follows/delete follow)}
                 {:hx-post (url-for :follows/create)}
      (if follow
        icons/person-dash-fill)
        icons/person-plus))]])

Then you can re-use some of the generated joy code. It’s super important to not do what I did before and to scope the follow by the account, you don’t want to let anyone unfollow anyone on their behalf!

(defn follow [req]
  (def {:params params :account account} req)

  ; # scope it
  (db/fetch [:account account :follow (get params :id)])))


(def follow-params
  (params :follow
    (validates [:followed-id] :required true)
    (permit [:followed-id])))


(defn follows/create [req]
  (def {:account account} req)

  (def params (follow-params req))

  (def result (-> (follow-params req)
                  (put :account-id (account :id))
                  (db/insert)
                  (rescue)))

  (def [errors follow] result)

  (text/html
    (follow-button {:id (params :followed-id)} follow)))


(defn follows/delete [req]
  (def follow (follow req))

  (db/delete follow)

  (text/html
    (follow-button {:id (follow :followed-id)})))

Look at all of that re-use, wow. You can’t teach that!

Profiles

This is the last feature that makes a complete social network and it fits within the controller model well at (route :get "/@*" :accounts/show)

This route uses wildcard params to get the account name (mostly because @:name doesn’t work currently).

Here’s how the route looks:

(defn accounts/show [req]
  (def {:wildcard params*} req)

  (when-let [account (db/find-by :account :where {:name (get params* 0)})]

    (def following (db/val "select count(id) from follow where followed_id = ?" (account :id)))
    (def followers (db/val "select count(id) from follow where follower_id = ?" (account :id)))
    (def likes (db/val "select count(id) from like where account_id = ?" (account :id)))
    (def num-posts (db/val "select count(id) from post where account_id = ?" (account :id)))
    (def posts (db/fetch-all [:account account :post] :order "post.created_at desc"))

    [:vstack {:spacing "s" :class "w-100"}

     [:img {:src (account :photo-url) :class "br-100 ba b--background-alt w-xl"}]

     [:vstack {:spacing "s"}

      [:vstack {:spacing "xs"}
       [:strong (account :display-name)]
       [:div {:class "muted"} (string "@" (account :name))]]

      [:hstack {:spacing "s"}

       [:hstack {:spacing "xs"}
        [:b following]
        [:span {:class "muted"} "Following"]]

       [:hstack {:spacing "xs"}
        [:b followers]
        [:span {:class "muted"} "Followers"]]

       [:hstack {:spacing "xs"}
        [:b likes]
        [:span {:class "muted"} "Likes"]]]]

     [:vstack
      (foreach [p posts]
        (post (merge req {:post p})))]]))

At the end I kind of just punted on the sql stuff. Eventually joy should have more sql helpers, like count, max, distinct, group.

Conclusion

I cut it short because twitter has quite a few more functions, but this is the gist of it, or the gist of any follower based social network. There’s no recommendation algorithm here, which is probably a good exercise with the rise of interest based social networks like tiktok. Who wants to curate their followers up front when you can discover them based on a few interests you pick? NO ONE that’s who.

A few takeaways from this exercise:

Oh, the source. It’s on github of course