Where is the chirp repo for the real-time Twitter clone in 15 minutes by Chris McCord?

So I saw this blog from Chris McCord, but I can’t find the repo for it… Anyone knows if the official one exists?

1 Like

Here you go:

https://github.com/dersnek/chirp

7 Likes

Thank you for sharing!!!

1 Like

Around seven minutes in the ‘likes’, ‘reposts’, 'edits", and ‘delete’ are added to the component with Font Awesome icons. I have gone through this twice, from the beginning, including deleting the db, and I can’t figure out where I am missing it.
Does anyone know where he is adding Font Awesome to the project?
I am a noob so I am sure I am missing something simple.

1 Like

Hi Bill.

To follow it through:

  1. We see the root .eex template loads an app.css in (lib/chirp_web/templates/layout/root.html.leex).
  2. That file gets built by the asset pipeline and therefore must come from the Webpack build, so we check the assets folder.
  3. If you open the package.json there, you’ll see the font awesome package is required from npm, so that’s how it gets installed.
  4. Open the assets/css/app.css file and you’ll see the font awesome files “included” at the top. Webpack picks up these imports and inlines the requested files into the built app.css. The same one we initially saw in Point 1.

Point 4 can be seen on GitHub here.

2 Likes

Thanks so much for the response! I have learned from it.
I made the mistake of running around github looking for examples before I came back to reread your last sentence. That made all of the confusion on my part go away. Thanks for the example!

2 Likes

I was doing these steps and It was needed for me added this on the file webpack.config.js:

// Load fonts
{
test: /.(woff(2)?|ttf|eot|svg)(?v=\d+.\d+.\d+)?$/,
use: [{
loader: ‘file-loader’,
options: {
name: ‘[name].[ext]’,
outputPath: ‘…/fonts’
},
}],
},
]
},

and then:

cd assets && npm install && node node_modules/webpack/bin/webpack.js --mode development

3 Likes

any one know how to live render when post deleted from this example ?
looks like phx-update=“prepend” does not work with deleted post ,
or is there another solution ?

1 Like

The mine works you need a double click on the garbage icon:


If you look the file live/post_live/index.ex

Has the "delete" event

@impl true
  def handle_event("delete", %{"id" => id}, socket) do
    post = Timeline.get_post!(id)
    {:ok, _} = Timeline.delete_post(post)

    {:noreply, assign(socket, :posts, fetch_posts())}
  end
3 Likes

I use https://github.com/dersnek/chirp.
Thanks! :grinning: :rocket:
If I use it as-is, I does not work with deleted post too.

It works with the following changes.

lib/chirp_web/live/post_live/index.ex
{:ok, assign(socket, :posts, fetch_posts()), temporary_assigns: [posts: []]}

{:ok, assign(socket, :posts, fetch_posts())}

lib/chirp_web/live/post_live/index.html.leex
<div id="posts" phx-update="prepend">

<div id="posts" phx-update="replace">

However, the original source code was showed in the video, so it may cause another problem.
Also, this change will not reflect the deletion in other browsers.

In addition to the changes above, the following changes make deletion in another browser.

lib/chirp/timeline.ex

def delete_post(%Post{} = post) do
  {:ok, %Post{}} = Repo.delete(post)
  broadcast({:ok, post}, :post_deleted)
end

lib/chirp_web/live/post_live/index.ex

def handle_event("delete", %{"id" => id}, socket) do
  post = Timeline.get_post!(id)
  {:ok, _} = Timeline.delete_post(post)

  {:noreply, socket}
end

def handle_info({:post_deleted, post}, socket) do
  {:noreply, update(socket, :posts, fn posts -> posts -- [post] end)}
end
5 Likes

Thx for the hint

My solution:

…/timeline.ex

def delete_post(%Post{} = post) do
    Repo.delete(post)
    |>broadcast(:post_deleted)
  end

/post_live/index.ex

def mount(_params, _session, socket) do
    if connected?(socket), do: Timeline.subscribe()
    # {:ok, assign(socket, :posts, fetch_posts()), temporary_assigns: [posts: []]}
    {:ok, assign(socket, :posts, fetch_posts())}
  end
.
.
.

def handle_info({:post_updated, updated_post}, socket) do
    {:noreply, update(socket, :posts, fn posts ->
      for post <- posts do
        case post.id == updated_post.id do
          true -> updated_post
          _ -> post
        end
      end
    end)}
  end

  def handle_info({:post_deleted, deleted_post}, socket) do
    {:noreply,
     update(socket, :posts, fn posts ->
       posts
       |> Enum.reject(fn post ->
         post.id == deleted_post.id
       end)
     end)}
  end

/post_live/index.html.leex

<div id="posts" phx-update="replace">
  <%= for post <- @posts do %>
    <%= live_component @socket, ChripWeb.PostLive.PostComponent, id: post.id, post: post %>
  <% end %>
</div>

hmm still not sure what combination to use (temporary_assigns + phx_update=“prepend or append”) or with “replace”

5 Likes

Great! It worked for me!

1 Like

This is true but it’s complaining an error when I update and click the :like or the :repost.
And I notice more slowly for the like and repost events.
The error when updates, but after the error it works:

[debug] QUERY OK db=1.4ms queue=0.3ms idle=1089.3ms
UPDATE "posts" SET "body" = $1, "updated_at" = $2 WHERE "id" = $3 ["My post!!!!", ~N[2020-04-30 00:58:50], 30]
[error] GenServer #PID<0.618.0> terminating
** (RuntimeError) found duplicate ID 30 for component ChirpWeb.PostLive.PostComponent when rendering template
    (phoenix_live_view 0.12.1) lib/phoenix_live_view/diff.ex:401: anonymous fn/5 in Phoenix.LiveView.Diff.render_pending_components/5
    (elixir 1.10.0) lib/enum.ex:2111: Enum."-reduce/3-lists^foldl/2-0-"/3
    (stdlib 3.10) maps.erl:232: :maps.fold_1/3
    (phoenix_live_view 0.12.1) lib/phoenix_live_view/diff.ex:392: Phoenix.LiveView.Diff.render_pending_components/5
    (phoenix_live_view 0.12.1) lib/phoenix_live_view/diff.ex:95: Phoenix.LiveView.Diff.render/3
    (phoenix_live_view 0.12.1) lib/phoenix_live_view/channel.ex:504: Phoenix.LiveView.Channel.render_diff/2
    (phoenix_live_view 0.12.1) lib/phoenix_live_view/channel.ex:361: Phoenix.LiveView.Channel.handle_changed/4
    (stdlib 3.10) gen_server.erl:637: :gen_server.try_dispatch/4
    (stdlib 3.10) gen_server.erl:711: :gen_server.handle_msg/6
    (stdlib 3.10) proc_lib.erl:259: :proc_lib.wake_up/3
Last message: {:post_updated, %Chirp.Timeline.Post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">, body: "My post!!!!", id: 30, inserted_at: ~N[2020-04-30 00:58:38], likes_count: 0, reposts_count: 0, updated_at: ~N[2020-04-30 00:58:50], username: "romenigld"}}
State: %{components: {%{{ChirpWeb.PostLive.PostComponent, 30} => {2, %{flash: %{}, id: 30, myself: 2, post: %Chirp.Timeline.Post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">, body: "My post!", id: 30, inserted_at: ~N[2020-04-30 00:58:38], likes_count: 0, reposts_count: 0, updated_at: ~N[2020-04-30 00:58:38], username: "romenigld"}}, %{}, {128518509547916470656476619130439709679, %{}}}}, %{2 => {ChirpWeb.PostLive.PostComponent, 30}}, 3}, join_ref: "22", serializer: Phoenix.Socket.V2.JSONSerializer, socket: #Phoenix.LiveView.Socket<assigns: %{flash: %{}, live_action: :index, live_module: ChirpWeb.PostLive.Index, page_title: "Listing Posts", post: nil, posts: [%Chirp.Timeline.Post{...}]}, changed: %{}, endpoint: ChirpWeb.Endpoint, id: "phx-FgpyvggJDgijrwCC", parent_pid: nil, root_pid: #PID<0.618.0>, router: ChirpWeb.Router, view: ChirpWeb.PostLive.Index, ...>, topic: "lv:phx-FgpyvggJDgijrwCC", transport_pid: #PID<0.549.0>, uri: %URI{authority: nil, fragment: nil, host: "localhost", path: nil, port: 4000, query: nil, scheme: "http", userinfo: nil}}
[info] GET /posts
[debug] Processing with Phoenix.LiveView.Plug.index/2
  Parameters: %{}
  Pipelines: [:browser]
[info] Sent 200 in 417µs
[debug] QUERY OK source="posts" db=0.5ms queue=0.1ms idle=1140.3ms
SELECT p0."id", p0."body", p0."likes_count", p0."reposts_count", p0."username", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 ORDER BY p0."id" DESC []
[debug] QUERY OK source="posts" db=0.4ms idle=1014.1ms
SELECT p0."id", p0."body", p0."likes_count", p0."reposts_count", p0."username", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 ORDER BY p0."id" DESC []

error for the like:

[debug] QUERY OK source="posts" db=1.7ms idle=1860.9ms
UPDATE "posts" AS p0 SET "likes_count" = p0."likes_count" + $1 WHERE (p0."id" = $2) RETURNING p0."id", p0."body", p0."likes_count", p0."reposts_count", p0."username", p0."inserted_at", p0."updated_at" [1, 30]
[error] GenServer #PID<0.639.0> terminating
** (RuntimeError) found duplicate ID 30 for component ChirpWeb.PostLive.PostComponent when rendering template
    (phoenix_live_view 0.12.1) lib/phoenix_live_view/diff.ex:401: anonymous fn/5 in Phoenix.LiveView.Diff.render_pending_components/5
    (elixir 1.10.0) lib/enum.ex:2111: Enum."-reduce/3-lists^foldl/2-0-"/3
    (stdlib 3.10) maps.erl:232: :maps.fold_1/3
    (phoenix_live_view 0.12.1) lib/phoenix_live_view/diff.ex:392: Phoenix.LiveView.Diff.render_pending_components/5
    (phoenix_live_view 0.12.1) lib/phoenix_live_view/diff.ex:95: Phoenix.LiveView.Diff.render/3
    (phoenix_live_view 0.12.1) lib/phoenix_live_view/channel.ex:504: Phoenix.LiveView.Channel.render_diff/2
    (phoenix_live_view 0.12.1) lib/phoenix_live_view/channel.ex:361: Phoenix.LiveView.Channel.handle_changed/4
    (stdlib 3.10) gen_server.erl:637: :gen_server.try_dispatch/4
    (stdlib 3.10) gen_server.erl:711: :gen_server.handle_msg/6
    (stdlib 3.10) proc_lib.erl:259: :proc_lib.wake_up/3
Last message: {:post_updated, %Chirp.Timeline.Post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">, body: "My post!!!!", id: 30, inserted_at: ~N[2020-04-30 00:58:38], likes_count: 1, reposts_count: 0, updated_at: ~N[2020-04-30 00:58:50], username: "romenigld"}}
State: %{components: {%{{ChirpWeb.PostLive.PostComponent, 30} => {0, %{flash: %{}, id: 30, myself: 0, post: %Chirp.Timeline.Post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">, body: "My post!!!!", id: 30, inserted_at: ~N[2020-04-30 00:58:38], likes_count: 0, reposts_count: 0, updated_at: ~N[2020-04-30 00:58:50], username: "romenigld"}}, %{}, {128518509547916470656476619130439709679, %{}}}}, %{0 => {ChirpWeb.PostLive.PostComponent, 30}}, 1}, join_ref: "28", serializer: Phoenix.Socket.V2.JSONSerializer, socket: #Phoenix.LiveView.Socket<assigns: %{flash: %{}, live_action: :index, live_module: ChirpWeb.PostLive.Index, page_title: "Listing Posts", post: nil, posts: [%Chirp.Timeline.Post{...}]}, changed: %{}, endpoint: ChirpWeb.Endpoint, id: "phx-FgpyvggJDgijrwCC", parent_pid: nil, root_pid: #PID<0.639.0>, router: ChirpWeb.Router, view: ChirpWeb.PostLive.Index, ...>, topic: "lv:phx-FgpyvggJDgijrwCC", transport_pid: #PID<0.549.0>, uri: %URI{authority: nil, fragment: nil, host: "localhost", path: nil, port: 4000, query: nil, scheme: "http", userinfo: nil}}
[error] GenServer #PID<0.638.0> terminating
** (RuntimeError) found duplicate ID 30 for component ChirpWeb.PostLive.PostComponent when rendering template
    (phoenix_live_view 0.12.1) lib/phoenix_live_view/diff.ex:401: anonymous fn/5 in Phoenix.LiveView.Diff.render_pending_components/5
    (elixir 1.10.0) lib/enum.ex:2111: Enum."-reduce/3-lists^foldl/2-0-"/3
    (stdlib 3.10) maps.erl:232: :maps.fold_1/3
    (phoenix_live_view 0.12.1) lib/phoenix_live_view/diff.ex:392: Phoenix.LiveView.Diff.render_pending_components/5
    (phoenix_live_view 0.12.1) lib/phoenix_live_view/diff.ex:95: Phoenix.LiveView.Diff.render/3
    (phoenix_live_view 0.12.1) lib/phoenix_live_view/channel.ex:504: Phoenix.LiveView.Channel.render_diff/2
    (phoenix_live_view 0.12.1) lib/phoenix_live_view/channel.ex:361: Phoenix.LiveView.Channel.handle_changed/4
    (stdlib 3.10) gen_server.erl:637: :gen_server.try_dispatch/4
    (stdlib 3.10) gen_server.erl:711: :gen_server.handle_msg/6
    (stdlib 3.10) proc_lib.erl:259: :proc_lib.wake_up/3
Last message: {:post_updated, %Chirp.Timeline.Post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">, body: "My post!!!!", id: 30, inserted_at: ~N[2020-04-30 00:58:38], likes_count: 1, reposts_count: 0, updated_at: ~N[2020-04-30 00:58:50], username: "romenigld"}}
State: %{components: {%{{ChirpWeb.PostLive.PostComponent, 30} => {0, %{flash: %{}, id: 30, myself: 0, post: %Chirp.Timeline.Post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">, body: "My post!!!!", id: 30, inserted_at: ~N[2020-04-30 00:58:38], likes_count: 0, reposts_count: 0, updated_at: ~N[2020-04-30 00:58:50], username: "romenigld"}}, %{}, {128518509547916470656476619130439709679, %{}}}}, %{0 => {ChirpWeb.PostLive.PostComponent, 30}}, 1}, join_ref: "132", serializer: Phoenix.Socket.V2.JSONSerializer, socket: #Phoenix.LiveView.Socket<assigns: %{flash: %{"info" => "Post updated successfully"}, live_action: :index, live_module: ChirpWeb.PostLive.Index, page_title: "Listing Posts", post: nil, posts: [%Chirp.Timeline.Post{...}]}, changed: %{}, endpoint: ChirpWeb.Endpoint, id: "phx-FgpzD-VLX7ir3ABE", parent_pid: nil, root_pid: #PID<0.638.0>, router: ChirpWeb.Router, view: ChirpWeb.PostLive.Index, ...>, topic: "lv:phx-FgpzD-VLX7ir3ABE", transport_pid: #PID<0.542.0>, uri: %URI{authority: nil, fragment: nil, host: "localhost", path: nil, port: 4000, query: nil, scheme: "http", userinfo: nil}}
[debug] QUERY OK source="posts" db=0.8ms idle=1873.8ms
SELECT p0."id", p0."body", p0."likes_count", p0."reposts_count", p0."username", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 ORDER BY p0."id" DESC []
[debug] QUERY OK source="posts" db=1.4ms queue=0.2ms idle=1876.4ms
SELECT p0."id", p0."body", p0."likes_count", p0."reposts_count", p0."username", p0."inserted_at", p0."updated_at" FROM "posts" AS p0 ORDER BY p0."id" DESC []

It complains for this but do the like. why??

looks like

    {:ok, assign(socket, :posts, fetch_posts())} 

is saving the lists to memory, u can try this solution:

def handle_info({:post_updated, updated_post}, socket) do
    {:noreply, update(socket, :posts, fn posts ->
      for post <- posts do
        case post.id == updated_post.id do
          true -> updated_post
          _ -> post
        end
      end
    end)}
  end

  def handle_info({:post_deleted, deleted_post}, socket) do
    {:noreply,
     update(socket, :posts, fn posts ->
       posts
       |> Enum.reject(fn post ->
         post.id == deleted_post.id
       end)
     end)}
  end
1 Like

Anyone know why presented in the video css classes worked out?

For example column-10 and column-90
I didn’t found definition of that classes in that repo or any of libraries source code on phoenix and ** phoenix_live_view

Is phoenix use some css framework by default?

Hello and welcome,

By default Phoenix uses Milligram, and You might find it in assets/css/phoenix.css.

1 Like

Thanks, now it make sense :slight_smile:

I forked the dersnek repo and backed out the code that Chris types in for anyone that wants to ‘code along’ without having to figure out how to add the icons to the project.

5 Likes

solution for delete

Thank you! I was wondering on why the :posts were empty during :handle_info calls and I think this explains why.

1 Like