{"id":24556358,"url":"https://github.com/dwyl/phoenix-liveview-chat-example","last_synced_at":"2025-10-04T07:32:33.210Z","repository":{"id":38258229,"uuid":"429753099","full_name":"dwyl/phoenix-liveview-chat-example","owner":"dwyl","description":"💬 Step-by-step tutorial creates a Chat App using Phoenix LiveView including Presence, Authentication and Style with Tailwind CSS","archived":false,"fork":false,"pushed_at":"2025-01-14T16:12:19.000Z","size":461,"stargazers_count":134,"open_issues_count":8,"forks_count":12,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-01-14T18:12:39.369Z","etag":null,"topics":["auth","authentication","chat","elixir","example","liveview","phoenix","phoenix-framework","phoenix-liveview","realtime","tutorial"],"latest_commit_sha":null,"homepage":"https://liveview-chat-example.fly.dev/","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dwyl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-11-19T10:15:04.000Z","updated_at":"2025-01-14T16:12:15.000Z","dependencies_parsed_at":"2023-02-15T13:01:15.451Z","dependency_job_id":"e413e2ff-9993-4f56-819f-004dc40cacb4","html_url":"https://github.com/dwyl/phoenix-liveview-chat-example","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwyl%2Fphoenix-liveview-chat-example","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwyl%2Fphoenix-liveview-chat-example/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwyl%2Fphoenix-liveview-chat-example/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dwyl%2Fphoenix-liveview-chat-example/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dwyl","download_url":"https://codeload.github.com/dwyl/phoenix-liveview-chat-example/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":235227506,"owners_count":18956141,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["auth","authentication","chat","elixir","example","liveview","phoenix","phoenix-framework","phoenix-liveview","realtime","tutorial"],"created_at":"2025-01-23T05:00:47.445Z","updated_at":"2025-10-04T07:32:27.860Z","avatar_url":"https://github.com/dwyl.png","language":"Elixir","readme":"\u003cdiv align=\"center\"\u003e\n\n# `LiveView` Chat _Tutorial_ \n\n![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/dwyl/phoenix-liveview-chat-example/ci.yml?label=build\u0026style=flat-square\u0026branch=main)\n[![codecov test coverage](https://img.shields.io/codecov/c/github/dwyl/phoenix-liveview-chat-example/main.svg?style=flat-square)](https://codecov.io/github/dwyl/phoenix-liveview-chat-example?branch=main)\n[![HitCount](https://hits.dwyl.com/dwyl/phoenix-liveview-chat-example.svg?style=flat-square\u0026show=unique)](https://hits.dwyl.com/dwyl/phoenix-liveview-chat-example)\n\n![liveview-chat-with-tailwind-css](https://user-images.githubusercontent.com/194400/174119023-bb83f5f4-867c-4bfa-a005-26b39c700137.gif)\n\n\u003c/div\u003e\n\n# Why? 🤷\n\nWe _really_ wanted a **_Free_ \u0026 Open Source** \nreal-world example\nwith _full_ code,\ntests and auth. \u003cbr /\u003e\nWe wrote this \nso we could point \npeople in our team/community \nlearning **`Phoenix LiveView`** to it. \u003cbr /\u003e\nThis `LiveView` example/tutorial takes you from zero \nto **_fully_ functioning app**\nin **20 minutes**. \n\n# What? 💬\n\nHere is the table of contents \nof what you can expect to cover \nin this example/tutorial:\n- [`LiveView` Chat _Tutorial_](#liveview-chat-tutorial)\n- [Why? 🤷](#why-)\n- [What? 💬](#what-)\n- [Who? 👤](#who-)\n- [_How_? 💻](#how-)\n  - [0. Prerequisites](#0-prerequisites)\n  - [1. Create `Phoenix` App](#1-create-phoenix-app)\n  - [2. Create `live` Directory, `LiveView` Controller and Template](#2-create-live-directory-liveview-controller-and-template)\n  - [3. Update `router.ex`](#3-update-routerex)\n  - [4. Update Tests](#4-update-tests)\n  - [5. Migration and Schema](#5-migration-and-schema)\n  - [6 Update `mount/3` function](#6-update-mount3-function)\n  - [7. Update Template](#7-update-template)\n    - [7.1 Update the Test Assertion](#71-update-the-test-assertion)\n  - [8. Handle Message Creation Events](#8-handle-message-creation-events)\n    - [8.1 Test Message Creation Validation](#81-test-message-creation-validation)\n  - [9. PubSub](#9-pubsub)\n    - [9.1 Notify Connected Clients of New Messages](#91-notify-connected-clients-of-new-messages)\n    - [9.2 Update `mount/3`](#92-update-mount3)\n    - [9.3 Update `handle_event/3`](#93-update-handle_event3)\n    - [9.4 Create `handle_info/2`](#94-create-handle_info2)\n    - [9.5 Test Messages are Displaying](#95-test-messages-are-displaying)\n  - [10. Hooks](#10-hooks)\n  - [11. Optional: Temporary assigns](#11-optional-temporary-assigns)\n  - [12. Authentication](#12-authentication)\n    - [12.1 Create `AUTH_API_KEY`](#121-create-auth_api_key)\n    - [12.2 Install `auth_plug` ⬇️](#122-install-auth_plug-️)\n    - [12.3 Create the _Optional_ Auth Pipeline in `router.ex`](#123-create-the-optional-auth-pipeline-in-routerex)\n    - [12.4 Create `AuthController`](#124-create-authcontroller)\n    - [12.5 Create `on_mount/4` functions](#125-create-on_mount4-functions)\n  - [14. Presence](#14-presence)\n  - [15. Tailwind CSS Stylin'](#15-tailwind-css-stylin)\n- [What's _Next_?](#whats-next)\n\n# Who? 👤\n\nAnyone learning `Phoenix LiveView` \nwanting a self-contained tutorial\nincluding: \n`Setup`, `Testing`, `Authentication`, `Presence`,\n\n# _How_? 💻\n\n\n\n\n## 0. Prerequisites\n\nIt's _recommended_, \nthough _not required_, \nthat you follow the \n[**`LiveView` Counter Tutorial**](https://github.com/dwyl/phoenix-liveview-counter-tutorial)\nas this one is more advanced.\nAt least, checkout the list of \n[prerequisites](https://github.com/dwyl/phoenix-liveview-counter-tutorial#prerequisites-what-you-need-before-you-start-)\nso you know what you need to have\ninstalled on your computer before \nyou start this adventure!\n\nProvided you have \n**`Elixir`**, **`Phoenix`** \nand **`Postgres`** installed,\nyou're good to go!\n\n\u003cbr /\u003e\n\n## 1. Create `Phoenix` App\n\nStart by creating the new **`liveview_chat`** `Phoenix` application:\n\n```sh\nmix phx.new liveview_chat --no-mailer --no-dashboard\n```\n\nWe don't need `email` or `dashboard` features \nso we're excluding them from our app.\nYou can learn more about creating\nnew Phoenix apps by running:\n`mix help phx.new`\n\nRun `mix deps.get` to retrieve the dependencies.\nthen create the\n**`liveview_chat_dev` Postgres database**\nby running the command:\n\n```sh\nmix ecto.setup\n```\n\nYou should see output similar to the following:\n\n```sh\nThe database for LiveviewChat.Repo has been created\n\n14:20:19.71 [info]  Migrations already up\n```\n\nOnce that command succeeds \nYou should now be able to start the application\nby running the command:\n\n```sh\nmix phx.server\n```\n\nYou will see terminal output similar to the following:\n\n```sh\n[info] Running LiveviewChatWeb.Endpoint with cowboy 2.9.0 at 127.0.0.1:4000 (http)\n[debug] Downloading esbuild from https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.29.tgz\n[info] Access LiveviewChatWeb.Endpoint at http://localhost:4000\n[watch] build finished, watching for changes...\n```\n\nWhen you open the URL:\n[`http://localhost:4000`](http://localhost:4000)\nin your web browser you should see something similar to:\n\n![phx.server](https://user-images.githubusercontent.com/6057298/142623156-ab767540-2561-43e3-bc87-1c4f89778d21.png)\n\n\u003cbr /\u003e\n\n## 2. Create `live` Directory, `LiveView` Controller and Template\n\nCreate the `lib/liveview_chat_web/live` folder \nand the controller at \n`lib/liveview_chat_web/live/message_live.ex`:\n\n```elixir\ndefmodule LiveviewChatWeb.MessageLive do\n  use LiveviewChatWeb, :live_view\n\n  def mount(_params, _session, socket) do\n    {:ok, socket}\n  end\n\n  def render(assigns) do\n    LiveviewChatWeb.MessageView.render(\"messages.html\", assigns)\n  end\nend\n```\n\u003e **Note**: neither the file name nor the code \n\u003e has the word \"**controller**\" anywhere. \n\u003e Hopefully it's not confusing.\n\u003e It's a \"controller\" in the sense \n\u003e that it controls what happens in the app. \n\nA **`LiveView` controller** requires \nthe functions **`mount/3`** and **`render/1`** to be defined. \u003cbr /\u003e\nTo keep the controller simple the **`mount/3`**\nis just returning the `{:ok, socket}` tuple\nwithout any changes.\nThe **`render/1`** \ninvokes \n`LiveviewChatWeb.MessageView.render/2` (included with `Phoenix`)\nwhich renders the **`messages.html.heex`** `template`\nwhich we will define below.\n\nCreate the \n`lib/liveview_chat_web/views/message_view.ex`\nfile:\n\n```elixir\ndefmodule LiveviewChatWeb.MessageView do\n  use LiveviewChatWeb, :view\nend\n```\n\nThis is similar to regular `Phoenix` `view`;\nnothing special/interesting here.\n\nNext, create the \n**`lib/liveview_chat_web/templates/message`** \ndirectory,\nthen create  \n**`lib/liveview_chat_web/templates/message/messages.html.heex`**\nfile \nand add the following line of `HTML`:\n\n```html\n\u003ch1\u003eLiveView Message Page\u003c/h1\u003e\n```\n\nFinally, to make the **root layout** simpler, \nopen the \n`lib/liveview_chat_web/templates/layout/root.html.heex`\nfile and \nupdate the contents of the `\u003cbody\u003e` to:\n\n```html\n\u003cbody\u003e\n  \u003cheader\u003e\n    \u003csection class=\"container\"\u003e\n      \u003ch1\u003eLiveView Chat Example\u003c/h1\u003e\n    \u003c/section\u003e\n  \u003c/header\u003e\n  \u003c%= @inner_content %\u003e\n\u003c/body\u003e\n```\n\n## 3. Update `router.ex`\n\nNow that you've created the necessary files,\nopen the router\n`lib/liveview_chat_web/router.ex` \nreplace the default route `PageController` controller:\n\n```elixir\nget \"/\", PageController, :index\n```\n\nwith `MessageLive` controller:\n\n\n```elixir\nscope \"/\", LiveviewChatWeb do\n  pipe_through :browser\n\n  live \"/\", MessageLive\nend\n```\n\nNow if you refresh the page you should see the following:\n\n![live view page](https://user-images.githubusercontent.com/194400/172560880-86e92751-2c00-4daf-9e6a-b428dec344ea.png)\n\n## 4. Update Tests\n\nAt this point we have made a few changes \nthat mean our automated test suite will no longer pass ... \nRun the tests in your command line with the following command:\n```sh\nmix test\n```\n\nYou will see output similar to the following:\n\n```sh\nGenerated liveview_chat app\n..\n\n  1) test GET / (LiveviewChatWeb.PageControllerTest)\n     test/liveview_chat_web/controllers/page_controller_test.exs:4\n     Assertion with =~ failed\n     code:  assert html_response(conn, 200) =~ \"Welcome to Phoenix!\"\n     left:  \"\u003c!DOCTYPE html\u003e\u003chtml lang=\\\"en\\\"\u003e \u003chead\u003e \u003cmeta charset=\\\"utf-8\\\"\u003e \u003cmeta http-equiv=\\\"X-UA-Compatible\\\" content=\\\"IE=edge\\\"\u003e\n     \u003ctitle data-suffix=\\\" · Phoenix Framework\\\"\u003eLiveviewChat · Phoenix Framework\u003c/title\u003e \u003clink phx-track-static rel=\\\"stylesheet\\\" href=\\\"/assets/app.css\\\"\u003e    \u003cscript defer phx-track-static type=\\\"text/javascript\\\" src=\\\"/assets/app.js\\\"\u003e\u003c/script\u003e  \u003c/head\u003e  \n     \u003cbody\u003e \u003cheader\u003e \u003csection class=\\\"container\\\"\u003e \n     \u003ch1\u003eLiveView Chat Example\u003c/h1\u003e\u003c/section\u003e \u003c/header\u003e\n     \u003ch1\u003eLiveView Message Page\u003c/h1\u003e\u003c/main\u003e\u003c/div\u003e  \u003c/body\u003e\u003c/html\u003e\"\n     right: \"Welcome to Phoenix!\"\n     stacktrace:\n       test/liveview_chat_web/controllers/page_controller_test.exs:6: (test)\n\nFinished in 0.03 seconds (0.02s async, 0.01s sync)\n3 tests, 1 failure\n```\n\nThis is because the `page_controller_test.exs` \nis still expecting the homepage to contain the \n**`\"Welcome to Phoenix!\"`** text.\n\nLet's update the tests!\nCreate the \n**`test/liveview_chat_web/live`** \nfolder and the \n**`message_live_test.exs`** \nfile within it:\n**`test/liveview_chat_web/live/message_live_test.exs`**\n\nAdd the following test code to it:\n\n```elixir\ndefmodule LiveviewChatWeb.MessageLiveTest do\n  use LiveviewChatWeb.ConnCase\n  import Phoenix.LiveViewTest\n\n  test \"disconnected and connected mount\", %{conn: conn} do\n    conn = get(conn, \"/\")\n    assert html_response(conn, 200) =~ \"LiveView Message Page\"\n\n    {:ok, _view, _html} = live(conn)\n  end\nend\n```\n\nWe are testing that the `/` endpoint \nis accessible and has the text\n**`\"LiveView Message Page\"`** on the page.\n\nSee also the\n[LiveViewTest module](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html)\nfor more information about testing and liveView.\n\nFinally you can delete all the default generated code linked to the `PageController`:\n\n- `rm test/liveview_chat_web/controllers/page_controller_test.exs`\n- `rm lib/liveview_chat_web/controllers/page_controller.ex`\n- `rm test/liveview_chat_web/views/page_view_test.exs`\n- `rm lib/liveview_chat_web/views/page_view.ex`\n- `rm -r lib/liveview_chat_web/templates/page`\n\nYou can now run the test again with `mix test` command.\nYou should see the following (tests passing):\n\n```sh\nGenerated liveview_chat app\n...\n\nFinished in 0.1 seconds (0.06s async, 0.1s sync)\n3 tests, 0 failures\n\nRandomized with seed 841084\n```\n## 5. Migration and Schema\n\nWith the `LiveView` structure defined,\nwe can focus on creating messages.\nThe database will save the message \nand the name of the sender.\nLet's create a new schema and migration:\n\n```sh\nmix phx.gen.schema Message messages name:string message:string\n```\n\n\u003e **Note**: don't forget to run `mix ecto.migrate` \n\u003e to create the new `messages` table in the database.\n\nWe can now update the `Message` schema \nto add functions for creating new messages \nand listing the existing messages. \nWe'll also update the changeset\nto add requirements \nand validations on the message text.\nOpen the `lib/liveview_chat/message.ex` file \nand update the code with the following:\n\n```elixir\ndefmodule LiveviewChat.Message do\n  use Ecto.Schema\n  import Ecto.Changeset\n  import Ecto.Query\n  alias LiveviewChat.Repo\n  alias __MODULE__\n\n  schema \"messages\" do\n    field :message, :string\n    field :name, :string\n\n    timestamps()\n  end\n\n  @doc false\n  def changeset(message, attrs) do\n    message\n    |\u003e cast(attrs, [:name, :message])\n    |\u003e validate_required([:name, :message])\n    |\u003e validate_length(:message, min: 2)\n  end\n\n  def create_message(attrs) do\n    %Message{}\n    |\u003e changeset(attrs)\n    |\u003e Repo.insert()\n  end\n\n  def list_messages do\n    Message\n    |\u003e limit(20)\n    |\u003e order_by(desc: :inserted_at)\n    |\u003e Repo.all()\n  end\nend\n```\n\nWe have added the `validate_length` function \non the message input to ensure\nthat messages have at **_least_ 2 characters**. \nThis is just an example to show how\nthe `changeset` validation works \nwith the form on the `LiveView` page.\n\nWe then created the `create_message/1` \nand `list_messages/0` functions.\nSimilar to \n[phoenix-chat-example](https://github.com/dwyl/phoenix-chat-example/)\nwe `limit` the number of messages returned \nto the **_latest_ 20**.\n\n## 6 Update `mount/3` function\n\nOpen the \n`lib/liveview_chat_web/live/message_live.ex` \nfile \nand add the following line at line 3:\n\n```elixir\nalias LiveviewChat.Message\n```\n\nNext update the `mount/3` function in the\n`lib/liveview_chat_web/live/message_live.ex` \nfile to use\nthe `list_messages` function:\n\n```elixir\ndef mount(_params, _session, socket) do\n  messages = Message.list_messages() |\u003e Enum.reverse()\n  changeset = Message.changeset(%Message{}, %{})\n  {:ok, assign(socket, changeset: changeset, messages: messages)}\nend\n```\n\n`mount/3` will now get the list of `messages` \nand create a `changeset` \nthat will be used for the message form.\nWe then \n[assign](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#assign/2)\nthe `changeset` and the `messages` to the socket which will display them on the liveView page.\n\n## 7. Update Template\n\nUpdate the \n`messages.html.heex` \ntemplate to the following code:\n\n```html\n\u003cul id='msg-list' phx-update=\"append\"\u003e\n  \u003c%= for message \u003c- @messages do %\u003e\n    \u003cli id={\"msg-#{message.id}\"}\u003e\n      \u003cb\u003e\u003c%= message.name %\u003e:\u003c/b\u003e\n      \u003c%= message.message %\u003e\n    \u003c/li\u003e\n  \u003c% end %\u003e\n\u003c/ul\u003e\n\n\u003c.form let={f} for={@changeset} id=\"form\" phx-submit=\"new_message\" phx-hook=\"Form\"\u003e\n  \u003c%= text_input f, :name, id: \"name\", placeholder: \"Your name\", autofocus: \"true\"  %\u003e\n  \u003c%= error_tag f, :name %\u003e\n\n  \u003c%= text_input f, :message, id: \"msg\", placeholder: \"Your message\"  %\u003e\n  \u003c%= error_tag f, :message %\u003e\n\n  \u003c%= submit \"Send\"%\u003e\n\u003c/.form\u003e\n```\n\nIt first displays the new messages\nand then provides a form for people \nto `create` a new message.\n\nIf you refresh the page, \nyou should see the following:\n\n![image](https://user-images.githubusercontent.com/6057298/142882923-db490aea-5af6-49d4-9e45-38c75d05e234.png)\n\n\nThe `\u003c.form\u003e\u003c/.form\u003e` syntax is how to use the form\n[function component](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#content).\n\u003e A function component is any function\nthat receives an `assigns` map as argument\nand returns a rendered `struct` built with the `~H` sigil.\n\n### 7.1 Update the Test Assertion\n\nFinally let's make sure the test are still passing by updating the `assert` \nin the `test/liveview_chat_web/live/message_live_test.exs` file\nto:\n\n```elixir\nassert html_response(conn, 200) =~ \"LiveView Chat\"\n```\n\nAs we have deleted the `LiveView Message Page` h1 title, \nwe can instead test for the title in the root layout \nand make sure the page is still displayed correctly.\n\n## 8. Handle Message Creation Events\n\nAt the moment if we run the `Phoenix` app `mix phx.server`\nand submit the form in the browser nothing will happen.\nIf we look at the server log, we see the following:\n\n```\n** (UndefinedFunctionError) function LiveviewChatWeb.MessageLive.handle_event/3\n  is undefined or private\n  (liveview_chat 0.1.0) LiveviewChatWeb.MessageLive.handle_event(\"new_message\",\n  %{\"_csrf_token\" =\u003e \"fyVPIls_XRBuGwlkMhxsFAciRRkpAVUOLW5k4UoR7JF1uZ5z2Dundigv\",\n  \"message\" =\u003e %{\"message\" =\u003e \"\", \"name\" =\u003e \"\"}}, #Phoenix.LiveView.Socket\n```\n\nOn submit the form is creating a new event defined with `phx-submit`:\n\n```elixir\n\u003c.form let={f} for={@changeset} id=\"form\" phx-submit=\"new_message\"\u003e\n```\n\nHowever this event is not managed on the server yet,\nwe can fix this by adding the\n`handle_event/3` function in\n`lib/liveview_chat_web/live/message_live.ex`:\n\n```elixir\ndef handle_event(\"new_message\", %{\"message\" =\u003e params}, socket) do\n  case Message.create_message(params) do\n    {:error, changeset} -\u003e\n      {:noreply, assign(socket, changeset: changeset)}\n\n    {:ok, _message} -\u003e\n      changeset = Message.changeset(%Message{}, %{\"name\" =\u003e params[\"name\"]})\n      {:noreply, assign(socket, changeset: changeset)}\n    end\nend\n```\n\nThe `create_message` function is called with the values from the form.\nIf an `error` occurs while trying to save the information in the database,\nfor example the `changeset` can return an error if the name or the `message` is\nempty or if the `message` is too short, the `changeset` is assigned again to the socket.\nThis will allow the form to display the `error` information:\n\n![name-cant-be-blank](https://user-images.githubusercontent.com/6057298/142921586-2ed0e7b4-c2a1-4cd2-ab87-154ff4e9f4d8.png)\n\nIf the message is saved without any errors,\nwe are creating a new changeset which contains the name from the form\nto avoid people having to enter their name again in the form, \nand we assign the new changeset to the socket.\n\n\n![chat-basic-message](https://user-images.githubusercontent.com/6057298/142921871-2feb20c2-906e-4640-8781-f8ea776dc05b.png)\n\n### 8.1 Test Message Creation Validation\n\nNow the form is displayed we can add the following tests\nto `test/liveview_chat_web/live/message_live_test.exs`:\n\n```elixir\n  import Plug.HTML, only: [html_escape: 1]\n\n  test \"name can't be blank\", %{conn: conn} do\n    {:ok, view, _html} = live(conn, \"/\")\n\n    assert view\n           |\u003e form(\"#form\", message: %{name: \"\", message: \"hello\"})\n           |\u003e render_submit() =~ html_escape(\"can't be blank\")\n  end\n\n  test \"message\", %{conn: conn} do\n    {:ok, view, _html} = live(conn, \"/\")\n\n    assert view\n           |\u003e form(\"#form\", message: %{name: \"Simon\", message: \"\"})\n           |\u003e render_submit() =~ html_escape(\"can't be blank\")\n  end\n\n  test \"minimum message length\", %{conn: conn} do\n    {:ok, view, _html} = live(conn, \"/\")\n\n    assert view\n           |\u003e form(\"#form\", message: %{name: \"Simon\", message: \"h\"})\n           |\u003e render_submit() =~ \"should be at least 2 character(s)\"\n  end\n```\n\nWe are using the\n[`form/3`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html#form/3)\nfunction to select the form and trigger\nthe submit event with different values for the name and the message.\nWe are testing that errors are properly displayed.\n\n## 9. PubSub\n\nInstead of having to reload the page to see the newly created messages,\nwe can use [PubSub](https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html) \n(**Pub**lish **Sub**scribe)\nto inform all connected clients \nthat a new message has been created and to\nupdate the UI to display the new message.\n\nOpen the `lib/liveview_chat/message.ex` file \nand add the following line near the top: \n```elixir\nalias Phoenix.PubSub\n```\n\nNext add the following 3 functions:\n\n```elixir\n  def subscribe() do\n    PubSub.subscribe(LiveviewChat.PubSub, \"liveview_chat\")\n  end\n\n  def notify({:ok, message}, event) do\n    PubSub.broadcast(LiveviewChat.PubSub, \"liveview_chat\", {event, message})\n  end\n\n  def notify({:error, reason}, _event), do: {:error, reason}\n```\n\n`subscribe/0` will be called when a client has properly displayed the liveView page\nand listen for new messages. \nIt is just a wrapper function for \n[Phoenix.PubSub.subscribe](https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html#subscribe/3).\n\n`notify/2` is invoked each time a new message is created \nto broadcast the message to the connected clients.\n`Repo.insert` can either returns `{:ok, message}` or `{:error, reason}`,\nso we need to define `notify/2` handle both cases.\n\n### 9.1 Notify Connected Clients of New Messages\n\nUpdate the `create_message/1` function in `message.ex`\nto invoke our newly created `notify/2` function:\n\n```elixir\n  def create_message(attrs) do\n    %Message{}\n    |\u003e changeset(attrs)\n    |\u003e Repo.insert()\n    |\u003e notify(:message_created)\n  end\n```\n\n### 9.2 Update `mount/3` \n\nWe can now connect the client \nwhen the `LiveView` page is rendered.\nAt the top of the \n`lib/liveview_chat_web/live/message_live.ex`\nfile,\nadd the following line:\n\n```elixir\nalias LiveviewChat.PubSub\n```\n\n\nThen update the `mount/3` function with:\n\n```elixir\ndef mount(_params, _session, socket) do\n  if connected?(socket), do: Message.subscribe()\n\n  messages = Message.list_messages() |\u003e Enum.reverse()\n  changeset = Message.changeset(%Message{}, %{})\n  {:ok, assign(socket, messages: messages, changeset: changeset)}\nend\n```\n\n`mount/3` now checks the socket is connected \nthen calls the new `Message.subscribe/0` function.\n\n\n### 9.3 Update `handle_event/3`\n\nSince the return value of `create_message/1` has changed,\nwe need to update `handle_event/3` to the following:\n\n```elixir\ndef handle_event(\"new_message\", %{\"message\" =\u003e params}, socket) do\n  case Message.create_message(params) do\n    {:error, changeset} -\u003e\n      {:noreply, assign(socket, changeset: changeset)}\n\n    :ok -\u003e # broadcast returns :ok (just the atom!) if there are no errors\n      changeset = Message.changeset(%Message{}, %{\"name\" =\u003e params[\"name\"]})\n      {:noreply, assign(socket, changeset: changeset)}\n  end\nend\n```\n### 9.4 Create `handle_info/2` \n\nThe last step \nis to handle the `:message_created` event \nby defining the `handle_info/2` function\nin `lib/liveview_chat_web/live/message_live.ex`:\n\n```elixir\ndef handle_info({:message_created, message}, socket) do\n  messages = socket.assigns.messages ++ [message]\n  {:noreply, assign(socket, messages: messages)}\nend\n```\n\nWhen the event is received, \nthe new message is added to the list of existing messages.\nThe new list is then assigned to the socket \nwhich will update the UI \nto display the new message.\n\n### 9.5 Test Messages are Displaying\n\nAdd the following tests to \n`test/liveview_chat_web/live/message_live_test.exs`\nto ensure that messages are correctly displayed on the page:\n\n```elixir\ntest \"message form submitted correctly\", %{conn: conn} do\n  {:ok, view, _html} = live(conn, \"/\")\n\n  assert view\n         |\u003e form(\"#form\", message: %{name: \"Simon\", message: \"hi\"})\n         |\u003e render_submit()\n\n  assert render(view) =~ \"\u003cb\u003eSimon:\u003c/b\u003e\"\n  assert render(view) =~ \"hi\"\nend\n\ntest \"handle_info/2\", %{conn: conn} do\n  {:ok, view, _html} = live(conn, \"/\")\n  assert render(view)\n  # send :created_message event when the message is created\n  Message.create_message(%{\"name\" =\u003e \"Simon\", \"message\" =\u003e \"hello\"})\n  # test that the name and the message is displayed\n  assert render(view) =~ \"\u003cb\u003eSimon:\u003c/b\u003e\"\n  assert render(view) =~ \"hello\"\nend\n```\n\nYou should now have a functional chat application using liveView!\nRun the `Phoenix` App with:\n\n```sh\nmix phx.server\n```\n\nVisit the App [`localhost:4000`](http://localhost:4000/)\nin 2 or more browsers,\nand send yourself some messages!\n\n![liveview-chat-demo](https://user-images.githubusercontent.com/194400/174016930-52b73247-eb7e-4c3e-8a4d-db0929aacc39.gif)\n\n\n## 10. Hooks\n\nOne issue we can notice is that the message input doesn't always\nreset to an empty value after sending a message using the `Enter` key\non the input field. This forces us to remove the\nprevious message manually before writing and sending a new one.\n\nThe reason is:\n\n\u003e The JavaScript client is always the source of truth for current input values.\nFor any given **input with focus**, `LiveView` will never overwrite\nthe input's current value, \neven if it deviates from the server's rendered updates.\nsee: https://hexdocs.pm/phoenix_live_view/form-bindings.html#javascript-client-specifics\n\n\nOur solution is to use `phx-hook` to run some javascript on the client\nafter one of the `LiveView` life-cycle callbacks \n(mounted, beforeUpdated, updated,\ndestroyed, disconnected, reconnected).\n\nLet's add a hook to monitor when the message form is `updated`.\nIn the `message.html.heex` file \nadd the `phx-hook` attribute to the `\u003c.form\u003e` element:\n\n```html\n\u003c.form let={f} for={@changeset} id=\"form\" phx-submit=\"new_message\" phx-hook=\"Form\"\u003e\n```\n\nThen in the `assets/js/app.js` file,\nadd the following `JavaScript` logic:\n\n\n```js\n// get message input element\nlet msg = document.getElementById('msg');                                           \n\n// define \"Form\" hook, the name must match the one\n// defined with phx-hoo=\"Form\"\nlet Hooks = {}\nHooks.Form = {\n  // Each time the form is updated run the code in the callback\n  updated() {\n    // If no error displayed reset the message value\n    if(document.getElementsByClassName('invalid-feedback').length == 0) {\n      msg.value = '';\n    }\n  }\n}\n\nlet csrfToken = document.querySelector(\"meta[name='csrf-token']\").getAttribute(\"content\")\nlet liveSocket = new LiveSocket(\"/live\", Socket, {params: {_csrf_token: csrfToken}, hooks: Hooks}) // Add hooks: Hooks\n```\n\nThe main logic to reset the message value is contained inside the `updated()`\ncallback function:\n\n```js\nif(document.getElementsByClassName('invalid-feedback').length == 0) {\n  msg.value = '';\n}\n```\n\nBefore setting the value to an empty string, \nwe check first that no errors are displayed \non the form by checking for the `invalid-feedback` CSS class.\n(read more about feedback:\n  https://hexdocs.pm/phoenix_live_view/form-bindings.html#phx-feedback-for )\n\nThe final step is to set the `hooks` on the `liveSocket` \nwith `hooks: Hooks`.\nThe message input should now be reset when a new message is added!\n\n\n## 11. Optional: Temporary assigns\n\nAt the moment the `mount/3` function first initializes the list of messages\nby loading the latest 20 messages from the database:\n\n```elixir\ndef mount(_params, _session, socket) do\n  if connected?(socket), do: Message.subscribe()\n\n  messages = Message.list_messages() |\u003e Enum.reverse() # get the list of messages\n  changeset = Message.changeset(%Message{}, %{})\n\n  {:ok, assign(socket, messages: messages, changeset: changeset)} ## assigns messages to socket\nend\n```\n\nThen each time a new message is created the `handle_info` function append\nthe message to the list of messages:\n\n```elixir\ndef handle_info({:message_created, message}, socket) do\n  messages = socket.assigns.messages ++ [message] # append new message to the existing list\n  {:noreply, assign(socket, messages: messages)}\nend\n```\n\nThis can cause issues if the list of messages becomes too long as\nall the messages are kept in memory on the server.\n\nTo minimize the use of the memory, \nwe can define messages as a temporary `assign`:\n\n```elixir\ndef mount(_params, _session, socket) do\n  if connected?(socket), do: Message.subscribe()\n\n  messages = Message.list_messages() |\u003e Enum.reverse()\n  changeset = Message.changeset(%Message{}, %{})\n\n  {:ok, assign(socket, messages: messages, changeset: changeset),\n  temporary_assigns: [messages: []]}\nend\n```\n\nThe list of messages is retrieved once, then it is reset to an empty list.\n\nNow the `handle_info/2` only needs to assign the new message to the socket:\n\n\n```elixir\ndef handle_info({:message_created, message}, socket) do\n  {:noreply, assign(socket, messages: [message])}\nend\n```\n\nFinally the `heex` messages template listens for any changes in the list of messages\nwith `phx-update` and appends the new message to the existing displayed list.\n\n```html\n\u003cul id='msg-list' phx-update=\"append\"\u003e\n   \u003c%= for message \u003c- @messages do %\u003e\n     \u003cli id={message.id}\u003e\n       \u003cb\u003e\u003c%= message.name %\u003e:\u003c/b\u003e\n       \u003c%= message.message %\u003e\n     \u003c/li\u003e\n   \u003c% end %\u003e\n\u003c/ul\u003e\n```\n\nSee also the Phoenix `temporary-assigns` documentation page:\nhttps://hexdocs.pm/phoenix_live_view/dom-patching.html#temporary-assigns\n\n\u003cbr /\u003e\n\n## 12. Authentication\n\nCurrently the `name` field \nis left to the person to define _manually_\nbefore they send a message.\nThis is fine in a basic demo app,\nbut we know we can do better.\nIn this section we'll add authentication \nusing\n[**`auth_plug`**](https://github.com/dwyl/auth_plug).\nThat will allow people using the App \nto authenticate with their `GitHub` or `Google` account\nand then pre-fill the `name` in the message form.\n\n### 12.1 Create `AUTH_API_KEY` \n\nAs per the \n[instructions](https://github.com/dwyl/auth_plug#2-get-your-auth_api_key-) \nfirst create a new **API Key** at \nhttps://authdemo.fly.dev/\ne.g:\n\n![image](https://user-images.githubusercontent.com/194400/174044750-73dcb29a-b236-40d4-9a91-27144b675320.png)\n\nThen create an `.env` file\nand add your new created api key:\n\n```.env\nexport AUTH_API_KEY=88SwQGzaZoJYXs6ihvwMy2dRVtm6KVeg4tSCjRKtwDvMUYUbi/88SwQDatWtSTMd2rKPnaZsAWFNpbf4vv2ZK7JW2nwuSypMeg/authdemo.fly.dev\n```\n\n\u003e **Note**: for security reasons, this is not a valid API key.\n\u003e Please create your own, it's free and takes less than a minute.\n\n### 12.2 Install `auth_plug` ⬇️\n\nAdd the [auth_plug](https://github.com/dwyl/auth_plug) package to your dependencies.\nIn `mix.exs` file update your `deps` function and add:\n\n```elixir\n{:auth_plug, \"~\u003e 1.4.10\"}\n```\n\nThis dependency will create new sessions for you \nand communicate with the dwyl `auth` application.\n\nDon't forget to:\n- load your key: `source .env`\n- get the dependencies: `mix deps.get`\n\nMake sure the `AUTH_API_KEY` is accessible\nbefore the new dependency is compiled. \u003cbr /\u003e\nYou can recompile the dependencies with `mix deps.compile --force`.\n\nNow we can start adding the authentication feature.\n### 12.3 Create the _Optional_ Auth Pipeline in `router.ex`\n\nTo allow [unauthenticated] \"guest\" users \naccess to the chat\nwe use the `AuthPlugOptional` plug.\nRead more at [optional auth](https://github.com/dwyl/auth_plug#optional-auth).\n\nIn the `router.ex` file, \nwe create a new `Plug` pipeline:\n\n```elixir\n# define the new pipeline using auth_plug\npipeline :authOptional, do: plug(AuthPlugOptional)\n```\n\nNext update the `scope \"/\", LiveviewChatWeb do` block\nto the following:\n\n```elixir\nscope \"/\", LiveviewChatWeb do\n  pipe_through [:browser, :authOptional]\n\n  live \"/\", MessageLive\n  get \"/login\", AuthController, :login\n  get \"/logout\", AuthController, :logout\nend\n```\n\nWe are now allowing authentication to be _optional_ \nfor all the routes in the router.\nEasy, hey? 😉\n\n### 12.4 Create `AuthController`\n\nCreate the `AuthController`\nwith both `login/2` and `logout/2` functions.\n\nCreate a new file:\n`lib/liveview_chat_web/controllers/auth_controller.ex`\nand add the following code:\n\n```elixir\ndefmodule LiveviewChatWeb.AuthController do\n  use LiveviewChatWeb, :controller\n\n  def login(conn, _params) do\n    redirect(conn, external: AuthPlug.get_auth_url(conn, \"/\"))\n  end\n\n  def logout(conn, _params) do\n    conn\n    |\u003e AuthPlug.logout()\n    |\u003e put_status(302)\n    |\u003e redirect(to: \"/\")\n  end\nend\n```\n\nThe `login/2` function \nredirects to the dwyl auth app.\nRead more about how to use the\n[`AuthPlug.get_auth_url/2`](https://hexdocs.pm/auth_plug/AuthPlug.html#get_auth_url/2)\nfunction.\nOnce authenticated the user will be redirected to the `/` endpoint\nand a `jwt` session is created on the client.\n\nThe `logout/2` function invokes `AuthPlug.logout/1` \nwhich removes the (JWT) session\nand redirects back to the homepage.\n\n### 12.5 Create `on_mount/4` functions\n\n`LiveView` provides the \n[`on_mount`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#on_mount/1)\ncallback that lets us run code \n_before_ the `mount`.\nWe'll use this callback to verify the `jwt` session \nand assign the `person` (`Map`)\nand `loggedin` (`boolean`) values to the `socket`.\n\nIn the\n`lib/liveview_chat_web/controllers/auth_controller.ex` file\nadd the following code \nto define two versions of `mount/4`:\n\n```elixir\n# import the assign_new function from LiveView\nimport Phoenix.LiveView, only: [assign_new: 3]\n\n# pattern match on :default auth and check session has jwt\ndef on_mount(:default, _params, %{\"jwt\" =\u003e jwt} = _session, socket) do\n  # verify and retrieve jwt stored data\n  claims = AuthPlug.Token.verify_jwt!(jwt)\n\n  # assigns the person and the loggedin values\n  socket =\n    socket\n    |\u003e assign_new(:person, fn -\u003e\n      AuthPlug.Helpers.strip_struct_metadata(claims)\n    end)\n    |\u003e assign_new(:loggedin, fn -\u003e true end)\n\n  {:cont, socket}\nend\n\n# when jwt is not defined just returns the current socket\ndef on_mount(:default, _params, _session, socket) do\n  socket = assign_new(socket, :loggedin, fn -\u003e false end)\n  {:cont, socket}\nend\n```\n\n[assign_new/3](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#assign_new/3)\nassigns a value to the socket if it doesn't exists.\n\nOnce the `on_mount/2` callback is defined,\nwe can call it in our \n`lib/liveview_chat_web/live/message_live.ex` \nfile:\n\n```elixir\ndefmodule LiveviewChatWeb.MessageLive do\n  use LiveviewChatWeb, :live_view\n  alias LiveviewChat.Message\n  # run authentication on mount\n  on_mount LiveviewChatWeb.AuthController\n```\n\nWe now have all the logic to let people authenticate,\nwe just need to update our root layout file\n`lib/liveview_chat_web/templates/layout/root.html.heex`\nto display a `login` (or `logout`) link:\n\n```html\n\u003cbody\u003e\n  \u003cheader\u003e\n    \u003csection class=\"container\"\u003e\n      \u003cnav\u003e\n        \u003cul\u003e\n          \u003c%= if @loggedin do %\u003e\n            \u003cli\u003e\n              \u003cimg width=\"40px\" src={@person.picture}/\u003e\n            \u003c/li\u003e\n            \u003cli\u003e\u003c%= link \"logout\", to: \"/logout\" %\u003e\u003c/li\u003e\n          \u003c% else %\u003e\n            \u003cli\u003e\u003c%= link \"Login\", to: \"/login\" %\u003e\u003c/li\u003e\n          \u003c% end %\u003e\n        \u003c/ul\u003e\n      \u003c/nav\u003e\n      \u003ch1\u003eLiveView Chat Example\u003c/h1\u003e\n    \u003c/section\u003e\n  \u003c/header\u003e\n  \u003c%= @inner_content %\u003e\n\u003c/body\u003e\n```\n\nIf the person is _not_ yet `loggedin` \nwe display a `login` link \notherwise the `logout` link is displayed.\n\nThe last step \nis to display the name of the logged-in person \nin the name field of the message form.\nFor that we can update the form changeset \nin the `mount` function to set the name parameters:\n\n```elixir\ndef mount(_params, _session, socket) do\n  if connected?(socket), do: Message.subscribe()\n\n  # add name parameter if loggedin\n  changeset =\n    if socket.assigns.loggedin do\n      Message.changeset(%Message{}, %{\"name\" =\u003e socket.assigns.person[\"givenName\"]})\n    else\n      Message.changeset(%Message{}, %{})\n    end\n\n  messages = Message.list_messages() |\u003e Enum.reverse()\n\n  {:ok, assign(socket, messages: messages, changeset: changeset),\n   temporary_assigns: [messages: []]}\nend\n```\n\nYou can now run the application and be able to login/logout!\n\n![logout-button](https://user-images.githubusercontent.com/194400/145076949-e8e7cebd-9b20-4d1f-b932-68a00977acec.png)\n\n\u003c!-- Yes, we know we skipped step 13 ... \n mostly to check if you're paying attention. 😜\n But also because some people find it \"unlucky\" ...\n https://en.wikipedia.org/wiki/13_(number)#Luck 🙄 --\u003e\n## 14. Presence\n\nIn this section we will use \n[**Phoenix Presence**](https://hexdocs.pm/phoenix/Phoenix.Presence.html)\nto display a list of people who are currently using the application.\n\nThe first step is to create the `lib/liveview_chat/presence.ex` file:\n\n```elixir\ndefmodule LiveviewChat.Presence do\n  use Phoenix.Presence,\n    otp_app: :liveview_chat,\n    pubsub_server: LiveviewChat.PubSub\nend\n```\n\nThen in `lib/liveview_chat/application.ex` \nwe add the newly created `Presence`\nmodule to the list of applications \nfor the supervisor to start:\n\n```elixir\n  def start(_type, _args) do\n    children = [\n      # Start the Ecto repository\n      LiveviewChat.Repo,\n      # Start the Telemetry supervisor\n      LiveviewChatWeb.Telemetry,\n      # Start the PubSub system\n      {Phoenix.PubSub, name: LiveviewChat.PubSub},\n      # Presence\n      LiveviewChat.Presence,\n      # Start the Endpoint (http/https)\n      LiveviewChatWeb.Endpoint\n      # Start a worker by calling: LiveviewChat.Worker.start_link(arg)\n      # {LiveviewChat.Worker, arg}\n    ]\n...\n```\n\nWe are now ready to use the Presence features in our liveview endpoint. \u003cbr /\u003e\nIn the `lib/liveview_chat_web/live/message_live.ex` file,\nupdate the `mount` function with the following:\n\n\n```elixir\n  @presence_topic \"liveview_chat_presence\"\n\n  def mount(_params, _session, socket) do\n    if connected?(socket) do\n      Message.subscribe()\n\n      {id, name} =\n        if socket.assigns.loggedin do\n          {socket.assigns.person[\"id\"], socket.assigns.person[\"givenName\"]}\n        else\n          {socket.id, \"guest\"}\n        end\n\n      {:ok, _} = Presence.track(self(), @presence_topic, id, %{name: name})\n      Phoenix.PubSub.subscribe(PubSub, @presence_topic)\n    end\n\n    changeset =\n      if socket.assigns.loggedin do\n        Message.changeset(%Message{}, %{\"name\" =\u003e socket.assigns.person[\"givenName\"]})\n      else\n        Message.changeset(%Message{}, %{})\n      end\n\n    messages = Message.list_messages() |\u003e Enum.reverse()\n\n    {:ok,\n     assign(socket,\n       messages: messages,\n       changeset: changeset,\n       presence: get_presence_names()\n     ), temporary_assigns: [messages: []]}\n  end\n```\n\nLet's recap the main changes to the `mount/3` function:\n\nFirst we create the module attribute `@presence_topic` \nto define the `topic` we'll use with the Presence functions.\n\n\nThe following part of the code defines a tuple \ncontaining an `id` of the person and their name.\n The name will default to \"guest\" if the person is _not_ loggedin.\n\n```elixir\n{id, name} =\n    if socket.assigns.loggedin do\n        {socket.assigns.person[\"id\"], socket.assigns.person[\"givenName\"]}\n     else\n        {socket.id, \"guest\"}\n    end\n```\n\nSecondly we use the [track/4](https://hexdocs.pm/phoenix/Phoenix.Presence.html#c:track/4) function\nto let Presence knows that a new client is looking at the application:\n\n```elixir\n{:ok, _} = Presence.track(self(), @presence_topic, id, %{name: name})\n```\n\nThird we use PubSub to listen to Presence changes (person joining or leaving the application):\n\n```elixir\nPhoenix.PubSub.subscribe(PubSub, @presence_topic)\n```\n\nFinally we create a new `presence` assign in the socket:\n\n```elixir\npresence: get_presence_names()\n```\n\n`get_presence_names` function will return a list of loggedin users and if any\nthe number of \"guest\" users.\n\n\nAdd the following code at the end of the `MessageLive` module:\n\n```elixir\n  defp get_presence_names() do\n    Presence.list(@presence_topic)\n    |\u003e Enum.map(fn {_k, v} -\u003e List.first(v.metas).name end)\n    |\u003e group_names()\n  end\n\n  # return list of names and number of guests\n  defp group_names(names) do\n    loggedin_names = Enum.filter(names, fn name -\u003e name != \"guest\" end)\n\n    guest_names =\n      Enum.count(names, fn name -\u003e name == \"guest\" end)\n      |\u003e guest_names()\n\n    if guest_names do\n      [guest_names | loggedin_names]\n    else\n      loggedin_names\n    end\n  end\n\n  defp guest_names(0), do: nil\n  defp guest_names(1), do: \"1 guest\"\n  defp guest_names(n), do: \"#{n} guests\"\n```\n\nThe important function call in the code above is `Presence.list(@presence_topic)`.\nThe [list/1](https://hexdocs.pm/phoenix/Phoenix.Presence.html#c:list/1) function\nreturns the list of users using the application.\nThe function `group_names` and `guest_names` are just here to manipulate the\nPresence data returned by `list`, see https://hexdocs.pm/phoenix/Phoenix.Presence.html#c:list/1-presence-data-structure\n\nSo far we've tracked new people using the chat page in the `mount` function and\nwe've been using PubSub to listen to presence changes.\nThe final step is to handle these changes by adding a `handle_info` function:\n\n```elixir\ndef handle_info(%{event: \"presence_diff\", payload: _diff}, socket) do\n  { :noreply, assign(socket, presence: get_presence_names())}\nend\n```\n\n\u003e Finally, a diff of presence join and leave events \nwill be sent to the clients as they happen in real-time \nwith the \"presence_diff\" event.\n\nThe `handle_info` function catches the `presence_diff` event and reassigns to the socket\nthe `presence` value with the result of the `get_presence_names` function call.\n\nTo display the names we add the following in the\n`lib/liveview_chat_web/templates/message/messages.html.heex`\ntemplate file:\n\n\n```html\n\u003cb\u003ePeople currently using the app:\u003c/b\u003e\n\u003cul\u003e\n   \u003c%= for name \u003c- @presence do %\u003e\n     \u003cli\u003e\n       \u003c%= name %\u003e\n     \u003c/li\u003e\n   \u003c% end %\u003e\n\u003c/ul\u003e\n```\n\nYou should now be able to run the application and see the loggedin users\nand the number of guest users.\n\nWe can test that the template has been properly updated by adding these two\ntests in `test/liveview_chat_web/live/message_live_test.exs` :\n\n```elixir\n  test \"1 guest online\", %{conn: conn} do\n    {:ok, view, _html} = live(conn, \"/\")\n\n    assert render(view) =~ \"1 guest\"\n  end\n\n  test \"2 guests online\", %{conn: conn} do\n    {:ok, _view, _html} = live(conn, \"/\")\n    {:ok, view2, _html} = live(conn, \"/\")\n\n    assert render(view2) =~ \"2 guests\"\n  end\n```\n\n## 15. Tailwind CSS Stylin'\n\nIf you're new to `Tailwind`,\nplease see: https://github.com/dwyl/learn-tailwind\n\nReplace the contents of `lib/liveview_chat_web/templates/layout/root.html.heex`\nwith:\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n  \u003chead\u003e\n    \u003cmeta charset=\"utf-8\"/\u003e\n    \u003cmeta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\"/\u003e\n    \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/\u003e\n    \u003cmeta name=\"csrf-token\" content={csrf_token_value()}\u003e\n    \u003c%= live_title_tag assigns[:page_title] || \"LiveviewChat\", suffix: \" · Phoenix Framework\" %\u003e\n    \u003cscript defer phx-track-static type=\"text/javascript\" src={Routes.static_path(@conn, \"/assets/app.js\")}\u003e\u003c/script\u003e\n    \u003cscript src=\"https://cdn.tailwindcss.com\"\u003e\u003c/script\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cheader class=\"bg-slate-800 w-full min-h-[15%] pt-5 pb-1 mb-2\"\u003e\n      \u003csection\u003e\n        \u003cnav\u003e\n          \u003cdiv class=\"text-white width-[10%] float-left ml-3 -mt-5 align-middle\"\u003e\n          \u003cb\u003ePeople in Chat:\u003c/b\u003e\n          \u003cul\u003e\n            \u003c%= for name \u003c- @presence do %\u003e\n              \u003cli\u003e\n                \u003c%= name %\u003e\n              \u003c/li\u003e\n            \u003c% end %\u003e\n          \u003c/ul\u003e\n          \u003c/div\u003e\n\n          \u003cul class=\"float-right mr-3\"\u003e\n            \u003c%= if @loggedin do %\u003e\n              \u003cli\u003e\n                \u003cimg width=\"42px\" src={@person.picture} class=\"-mt-3\"/\u003e\n              \u003c/li\u003e\n              \u003cli class=\"text-white\"\u003e\n                \u003c%= link \"logout\", to: \"/logout\" %\u003e\n              \u003c/li\u003e\n            \u003c% else %\u003e\n              \u003cli class=\"bg-green-600 text-white rounded-xl px-4 py-2 w-full mb-2 font-bold\"\u003e\n                \u003c%= link \"Login\", to: \"/login\" %\u003e\n              \u003c/li\u003e\n            \u003c% end %\u003e\n          \u003c/ul\u003e\n        \u003c/nav\u003e\n        \u003ch1 class=\"text-3xl mb-4 text-center font-mono text-white\"\u003eLiveView Chat Example\u003c/h1\u003e\n      \u003c/section\u003e\n    \u003c/header\u003e\n    \u003c%= @inner_content %\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n\n```\n\nAnd then replace the contents of \n`lib/liveview_chat_web/templates/message/messages.html.heex`\nwith:\n\n```html\n\u003cul id='msg-list' phx-update=\"append\"\u003e\n   \u003c%= for message \u003c- @messages do %\u003e\n     \u003cli id={\"msg-#{message.id}\"} class=\"px-5\"\u003e\n       \u003csmall class=\"float-right text-xs align-middle \"\u003e\n         \u003c%= message.inserted_at %\u003e\n       \u003c/small\u003e\n       \u003cb\u003e\u003c%= message.name %\u003e:\u003c/b\u003e\n       \u003c%= message.message %\u003e\n     \u003c/li\u003e\n   \u003c% end %\u003e\n\u003c/ul\u003e\n\n\u003cfooter class=\"fixed bottom-0 w-full bg-slate-300 pb-2 px-5 pt-2\"\u003e\n\u003c.form let={f} for={@changeset} id=\"form\" phx-submit=\"new_message\" phx-hook=\"Form\"\u003e\n\n  \u003c%= if @loggedin do %\u003e\n    \u003c%= text_input f, :name, id: \"name\", value: @person.givenName,\n    class: \"hidden\" %\u003e\n  \u003c% else %\u003e\n    \u003c%= text_input f, :name, id: \"name\", placeholder: \"Name\", autofocus: \"true\",\n     class: \"border p-2 w-9/12 mb-2 mt-2 mr2\" %\u003e\n     \u003cspan class=\"italic text-2xl ml-4\"\u003eor\u003c/span\u003e\n     \u003cspan class=\"bg-green-600 text-white rounded-xl px-4 py-2 mb-2 mt-3 float-right\"\u003e\n       \u003c%= link \"Login\", to: \"/login\" %\u003e\n     \u003c/span\u003e\n    \u003c%= error_tag f, :name %\u003e\n  \u003c% end %\u003e\n\n   \u003c%= text_input f, :message, id: \"msg\", placeholder: \"Message\",\n   class: \"border p-2 w-10/12  mb-2 mt-2 float-left\"  %\u003e\n   \u003cp class=\" text-amber-600\"\u003e\n    \u003c%= error_tag f, :message %\u003e\n   \u003c/p\u003e\n   \u003c%= submit \"Send\", class: \"bg-sky-600 text-white rounded-xl px-4 py-2 mt-2 float-right\" %\u003e\n \u003c/.form\u003e\n\u003c/footer\u003e\n```\n\nYou should now have a UI/layout that looks like this:\n\n![liveview-chat-with-tailwind-css](https://user-images.githubusercontent.com/194400/174119023-bb83f5f4-867c-4bfa-a005-26b39c700137.gif)\n\nIf you have questions about any of the **`Tailwind`** classes used,\nplease spend 2 mins Googling \nand then if you're still stuck, \n[open an issue](https://github.com/dwyl/learn-tailwind/issues).\n\n\u003cbr /\u003e\n\n# What's _Next_?\n\nIf you found this example useful, \nplease ⭐️ the GitHub repository\nso we (_and others_) know you liked it!\n\n\nHere are a few other repositories you might want to read:\n\n- [github.com/dwyl/**phoenix-chat-example**](https://github.com/dwyl/phoenix-chat-example) \n  A chat application using Phoenix Socket\n- [github.com/dwyl/**phoenix-liveview-counter-tutorial**](https://github.com/dwyl/phoenix-liveview-counter-tutorial)\n- [github.com/dwyl/**phoenix-liveview-todo-list-tutorial**](https://github.com/dwyl/phoenix-liveview-todo-list-tutorial)\n\n\nAny questions or suggestions? Do not hesitate to \n[open new issues](https://github.com/dwyl/phoenix-liveview-chat-example/issues)!\n\nThank you!","funding_links":[],"categories":["More Tests!"],"sub_categories":["Update the Tests for `GenServer` State"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdwyl%2Fphoenix-liveview-chat-example","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdwyl%2Fphoenix-liveview-chat-example","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdwyl%2Fphoenix-liveview-chat-example/lists"}