<%= get_flash(@conn, :info) %>
23 |<%= get_flash(@conn, :error) %>
24 | 25 | <%= @inner %> 26 | 27 |├── .gitignore ├── README.md ├── brunch-config.js ├── config ├── config.exs ├── dev.exs ├── prod.exs ├── prod.secret.exs └── test.exs ├── elixir_buildpack.config ├── lib ├── demo_chat.ex └── demo_chat │ ├── endpoint.ex │ └── repo.ex ├── mix.exs ├── mix.lock ├── package.json ├── priv └── repo │ └── seeds.exs ├── test ├── controllers │ └── page_controller_test.exs ├── support │ ├── channel_case.ex │ ├── conn_case.ex │ └── model_case.ex ├── test_helper.exs └── views │ ├── error_view_test.exs │ ├── layout_view_test.exs │ └── page_view_test.exs └── web ├── channels ├── room_channel.ex └── user_socket.ex ├── controllers └── page_controller.ex ├── router.ex ├── static ├── assets │ ├── favicon.ico │ ├── images │ │ └── phoenix.png │ └── robots.txt ├── css │ └── app.css └── js │ ├── app.js │ └── socket.js ├── templates ├── layout │ └── app.html.eex └── page │ └── index.html.eex ├── views ├── error_view.ex ├── layout_view.ex └── page_view.ex └── web.ex /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generate on crash by the VM 8 | erl_crash.dump 9 | 10 | # Static artifacts 11 | /node_modules 12 | 13 | # Since we are building assets from web/static, 14 | # we ignore priv/static. You may want to comment 15 | # this depending on your deployment strategy. 16 | /priv/static/ 17 | 18 | # The config/prod.secret.exs file by default contains sensitive 19 | # data and you should not commit it into version control. 20 | # 21 | # Alternatively, you may comment the line below and commit the 22 | # secrets file as long as you replace its contents by environment 23 | # variables. 24 | # /config/prod.secret.exs 25 | ======= 26 | /_build 27 | /deps 28 | erl_crash.dump 29 | *.ez 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DemoChat 2 | 3 | Chat demo written in Elixir and Phoenix 4 | 5 | To start your Phoenix app: 6 | 7 | 1. Install dependencies with `mix deps.get` 8 | 2. Create and migrate your database with `mix ecto.create && mix ecto.migrate` 9 | 3. Start Phoenix endpoint with `mix phoenix.server` 10 | 11 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 12 | 13 | Ready to run in production? Please [check our deployment guides](http://www.phoenixframework.org/docs/deployment). 14 | 15 | ## Learn more 16 | 17 | * Official website: http://www.phoenixframework.org/ 18 | * Guides: http://phoenixframework.org/docs/overview 19 | * Docs: http://hexdocs.pm/phoenix 20 | * Mailing list: http://groups.google.com/group/phoenix-talk 21 | * Source: https://github.com/phoenixframework/phoenix 22 | 23 | -------------------------------------------------------------------------------- /brunch-config.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | // See http://brunch.io/#documentation for docs. 3 | files: { 4 | javascripts: { 5 | joinTo: "js/app.js" 6 | 7 | // To use a separate vendor.js bundle, specify two files path 8 | // https://github.com/brunch/brunch/blob/stable/docs/config.md#files 9 | // joinTo: { 10 | // "js/app.js": /^(web\/static\/js)/, 11 | // "js/vendor.js": /^(web\/static\/vendor)|(deps)/ 12 | // } 13 | // 14 | // To change the order of concatenation of files, explicitly mention here 15 | // https://github.com/brunch/brunch/tree/master/docs#concatenation 16 | // order: { 17 | // before: [ 18 | // "web/static/vendor/js/jquery-2.1.1.js", 19 | // "web/static/vendor/js/bootstrap.min.js" 20 | // ] 21 | // } 22 | }, 23 | stylesheets: { 24 | joinTo: "css/app.css" 25 | }, 26 | templates: { 27 | joinTo: "js/app.js" 28 | } 29 | }, 30 | 31 | conventions: { 32 | // This option sets where we should place non-css and non-js assets in. 33 | // By default, we set this to "/web/static/assets". Files in this directory 34 | // will be copied to `paths.public`, which is "priv/static" by default. 35 | assets: /^(web\/static\/assets)/ 36 | }, 37 | 38 | // Phoenix paths configuration 39 | paths: { 40 | // Dependencies and current project directories to watch 41 | watched: [ 42 | "deps/phoenix/web/static", 43 | "deps/phoenix_html/web/static", 44 | "web/static", 45 | "test/static" 46 | ], 47 | 48 | // Where to compile files to 49 | public: "priv/static" 50 | }, 51 | 52 | // Configure your plugins 53 | plugins: { 54 | babel: { 55 | // Do not use ES6 compiler in vendor code 56 | ignore: [/web\/static\/vendor/] 57 | } 58 | }, 59 | 60 | modules: { 61 | autoRequire: { 62 | "js/app.js": ["web/static/js/app"] 63 | } 64 | }, 65 | 66 | npm: { 67 | enabled: true 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | # Configures the endpoint 9 | config :demo_chat, DemoChat.Endpoint, 10 | url: [host: "localhost"], 11 | root: Path.dirname(__DIR__), 12 | secret_key_base: "Q0uw+BJ8dn2B7IxMcKRMTGiRgFoTf4KZeotU+kSc10aDoNHdyeSEiY2jIUlMppbp", 13 | render_errors: [accepts: ~w(html json)], 14 | pubsub: [name: DemoChat.PubSub, 15 | adapter: Phoenix.PubSub.PG2] 16 | 17 | # Configures Elixir's Logger 18 | config :logger, :console, 19 | format: "$time $metadata[$level] $message\n", 20 | metadata: [:request_id] 21 | 22 | # Import environment specific config. This must remain at the bottom 23 | # of this file so it overrides the configuration defined above. 24 | import_config "#{Mix.env}.exs" 25 | 26 | # Configure phoenix generators 27 | config :phoenix, :generators, 28 | migration: true, 29 | binary_id: false 30 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we use it 8 | # with brunch.io to recompile .js and .css sources. 9 | config :demo_chat, DemoChat.Endpoint, 10 | http: [port: 4000], 11 | debug_errors: true, 12 | code_reloader: true, 13 | cache_static_lookup: false, 14 | check_origin: false, 15 | watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin"]] 16 | 17 | # Watch static and templates for browser reloading. 18 | config :demo_chat, DemoChat.Endpoint, 19 | live_reload: [ 20 | patterns: [ 21 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$}, 22 | ~r{web/views/.*(ex)$}, 23 | ~r{web/templates/.*(eex)$} 24 | ] 25 | ] 26 | 27 | # Do not include metadata nor timestamps in development logs 28 | config :logger, :console, format: "[$level] $message\n" 29 | 30 | # Set a higher stacktrace during development. 31 | # Do not configure such in production as keeping 32 | # and calculating stacktraces is usually expensive. 33 | config :phoenix, :stacktrace_depth, 20 34 | 35 | # Configure your database 36 | config :demo_chat, DemoChat.Repo, 37 | adapter: Ecto.Adapters.Postgres, 38 | username: "postgres", 39 | password: "postgres", 40 | database: "demo_chat_dev", 41 | hostname: "localhost", 42 | pool_size: 10 43 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, we configure the host to read the PORT 4 | # from the system environment. Therefore, you will need 5 | # to set PORT=80 before running your server. 6 | # 7 | # You should also configure the url host to something 8 | # meaningful, we use this information when generating URLs. 9 | # 10 | # Finally, we also include the path to a manifest 11 | # containing the digested version of static files. This 12 | # manifest is generated by the mix phoenix.digest task 13 | # which you typically run after static files are built. 14 | config :demo_chat, DemoChat.Endpoint, 15 | http: [port: {:system, "PORT"}], 16 | url: [host: "example.com", port: 80], 17 | cache_static_manifest: "priv/static/manifest.json" 18 | 19 | # Do not print debug messages in production 20 | config :logger, level: :info 21 | 22 | # ## SSL Support 23 | # 24 | # To get SSL working, you will need to add the `https` key 25 | # to the previous section and set your `:url` port to 443: 26 | # 27 | # config :demo_chat, DemoChat.Endpoint, 28 | # ... 29 | # url: [host: "example.com", port: 443], 30 | # https: [port: 443, 31 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 32 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")] 33 | # 34 | # Where those two env variables return an absolute path to 35 | # the key and cert in disk or a relative path inside priv, 36 | # for example "priv/ssl/server.key". 37 | # 38 | # We also recommend setting `force_ssl`, ensuring no data is 39 | # ever sent via http, always redirecting to https: 40 | # 41 | # config :demo_chat, DemoChat.Endpoint, 42 | # force_ssl: [hsts: true] 43 | # 44 | # Check `Plug.SSL` for all available options in `force_ssl`. 45 | 46 | # ## Using releases 47 | # 48 | # If you are doing OTP releases, you need to instruct Phoenix 49 | # to start the server for all endpoints: 50 | # 51 | # config :phoenix, :serve_endpoints, true 52 | # 53 | # Alternatively, you can configure exactly which server to 54 | # start per endpoint: 55 | # 56 | # config :demo_chat, DemoChat.Endpoint, server: true 57 | # 58 | 59 | # Finally import the config/prod.secret.exs 60 | # which should be versioned separately. 61 | import_config "prod.secret.exs" 62 | -------------------------------------------------------------------------------- /config/prod.secret.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # In this file, we keep production configuration that 4 | # you likely want to automate and keep it away from 5 | # your version control system. 6 | config :demo_chat, DemoChat.Endpoint, 7 | http: [port: {:system, "PORT"}], 8 | url: [host: "papo-reto-demo-chat.herokuapp.com", port: 80], 9 | cache_static_manifest: "priv/static/manifest.json", 10 | secret_key_base: System.get_env("SECRET_KEY_BASE") 11 | 12 | # Configure your database 13 | config :demo_chat, DemoChat.Repo, 14 | adapter: Ecto.Adapters.Postgres, 15 | username: System.get_env("DATABASE_USERNAME"), 16 | password: System.get_env("DATABASE_PASSWORD"), 17 | database: "demo_chat_prod", 18 | pool_size: 20 19 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :demo_chat, DemoChat.Endpoint, 6 | http: [port: 4001], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | 12 | # Configure your database 13 | config :demo_chat, DemoChat.Repo, 14 | adapter: Ecto.Adapters.Postgres, 15 | username: "postgres", 16 | password: "postgres", 17 | database: "demo_chat_test", 18 | hostname: "localhost", 19 | pool: Ecto.Adapters.SQL.Sandbox 20 | -------------------------------------------------------------------------------- /elixir_buildpack.config: -------------------------------------------------------------------------------- 1 | # Erlang version 2 | erlang_version=18.1 3 | 4 | # Elixir version 5 | elixir_version=1.1.1 6 | 7 | # Do dependencies have to be built from scratch on every deploy? 8 | always_build_deps=false 9 | 10 | # Export heroku config vars 11 | # config_vars_to_export=(DATABASE_URL SESSION_SECRET) 12 | -------------------------------------------------------------------------------- /lib/demo_chat.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoChat do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec, warn: false 8 | 9 | children = [ 10 | # Start the endpoint when the application starts 11 | supervisor(DemoChat.Endpoint, []), 12 | # Start the Ecto repository 13 | worker(DemoChat.Repo, []), 14 | # Here you could define other workers and supervisors as children 15 | # worker(DemoChat.Worker, [arg1, arg2, arg3]), 16 | ] 17 | 18 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 19 | # for other strategies and supported options 20 | opts = [strategy: :one_for_one, name: DemoChat.Supervisor] 21 | Supervisor.start_link(children, opts) 22 | end 23 | 24 | # Tell Phoenix to update the endpoint configuration 25 | # whenever the application is updated. 26 | def config_change(changed, _new, removed) do 27 | DemoChat.Endpoint.config_change(changed, removed) 28 | :ok 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/demo_chat/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoChat.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :demo_chat 3 | 4 | socket "/socket", DemoChat.UserSocket 5 | 6 | # Serve at "/" the static files from "priv/static" directory. 7 | # 8 | # You should set gzip to true if you are running phoenix.digest 9 | # when deploying your static files in production. 10 | plug Plug.Static, 11 | at: "/", from: :demo_chat, gzip: false, 12 | only: ~w(css fonts images js favicon.ico robots.txt) 13 | 14 | # Code reloading can be explicitly enabled under the 15 | # :code_reloader configuration of your endpoint. 16 | if code_reloading? do 17 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 18 | plug Phoenix.LiveReloader 19 | plug Phoenix.CodeReloader 20 | end 21 | 22 | plug Plug.RequestId 23 | plug Plug.Logger 24 | 25 | plug Plug.Parsers, 26 | parsers: [:urlencoded, :multipart, :json], 27 | pass: ["*/*"], 28 | json_decoder: Poison 29 | 30 | plug Plug.MethodOverride 31 | plug Plug.Head 32 | 33 | plug Plug.Session, 34 | store: :cookie, 35 | key: "_demo_chat_key", 36 | signing_salt: "QW8YbbYg" 37 | 38 | plug DemoChat.Router 39 | end 40 | -------------------------------------------------------------------------------- /lib/demo_chat/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoChat.Repo do 2 | use Ecto.Repo, otp_app: :demo_chat 3 | end 4 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule DemoChat.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :demo_chat, 6 | version: "0.0.1", 7 | elixir: "~> 1.0", 8 | elixirc_paths: elixirc_paths(Mix.env), 9 | compilers: [:phoenix] ++ Mix.compilers, 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | aliases: aliases, 13 | deps: deps] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [mod: {DemoChat, []}, 21 | applications: [:phoenix, :phoenix_html, :cowboy, :logger, 22 | :phoenix_ecto, :postgrex]] 23 | end 24 | 25 | # Specifies which paths to compile per environment. 26 | defp elixirc_paths(:test), do: ["lib", "web", "test/support"] 27 | defp elixirc_paths(_), do: ["lib", "web"] 28 | 29 | # Specifies your project dependencies. 30 | # 31 | # Type `mix help deps` for examples and options. 32 | defp deps do 33 | [{:phoenix, "~> 1.0.4"}, 34 | {:phoenix_ecto, "~> 1.1"}, 35 | {:postgrex, ">= 0.0.0"}, 36 | {:phoenix_html, "~> 2.1"}, 37 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 38 | {:cowboy, "~> 1.0"}] 39 | end 40 | 41 | # Aliases are shortcut or tasks specific to the current project. 42 | # For example, to create, migrate and run the seeds file at once: 43 | # 44 | # $ mix ecto.setup 45 | # 46 | # See the documentation for `Mix` for more info on aliases. 47 | defp aliases do 48 | ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 49 | "ecto.reset": ["ecto.drop", "ecto.setup"]] 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"cowboy": {:hex, :cowboy, "1.0.4"}, 2 | "cowlib": {:hex, :cowlib, "1.0.2"}, 3 | "decimal": {:hex, :decimal, "1.1.0"}, 4 | "ecto": {:hex, :ecto, "1.0.7"}, 5 | "fs": {:hex, :fs, "0.9.2"}, 6 | "phoenix": {:hex, :phoenix, "1.0.4"}, 7 | "phoenix_ecto": {:hex, :phoenix_ecto, "1.2.0"}, 8 | "phoenix_html": {:hex, :phoenix_html, "2.2.0"}, 9 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.1"}, 10 | "plug": {:hex, :plug, "1.0.3"}, 11 | "poison": {:hex, :poison, "1.5.0"}, 12 | "poolboy": {:hex, :poolboy, "1.5.1"}, 13 | "postgrex": {:hex, :postgrex, "0.9.1"}, 14 | "ranch": {:hex, :ranch, "1.2.0"}} 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": { 3 | }, 4 | "dependencies": { 5 | "brunch": "^1.8.5", 6 | "babel-brunch": "^5.1.1", 7 | "clean-css-brunch": ">= 1.0 < 1.8", 8 | "css-brunch": ">= 1.0 < 1.8", 9 | "javascript-brunch": ">= 1.0 < 1.8", 10 | "uglify-js-brunch": ">= 1.0 < 1.8" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # DemoChat.Repo.insert!(%SomeModel{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /test/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DemoChat.PageControllerTest do 2 | use DemoChat.ConnCase 3 | 4 | test "GET /" do 5 | conn = get conn(), "/" 6 | assert html_response(conn, 200) =~ "Welcome to Phoenix!" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoChat.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | imports other functionality to make it easier 8 | to build and query models. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | alias DemoChat.Repo 24 | import Ecto.Model 25 | import Ecto.Query, only: [from: 2] 26 | 27 | 28 | # The default endpoint for testing 29 | @endpoint DemoChat.Endpoint 30 | end 31 | end 32 | 33 | setup tags do 34 | unless tags[:async] do 35 | Ecto.Adapters.SQL.restart_test_transaction(DemoChat.Repo, []) 36 | end 37 | 38 | :ok 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoChat.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | imports other functionality to make it easier 8 | to build and query models. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | 23 | alias DemoChat.Repo 24 | import Ecto.Model 25 | import Ecto.Query, only: [from: 2] 26 | 27 | import DemoChat.Router.Helpers 28 | 29 | # The default endpoint for testing 30 | @endpoint DemoChat.Endpoint 31 | end 32 | end 33 | 34 | setup tags do 35 | unless tags[:async] do 36 | Ecto.Adapters.SQL.restart_test_transaction(DemoChat.Repo, []) 37 | end 38 | 39 | {:ok, conn: Phoenix.ConnTest.conn()} 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/support/model_case.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoChat.ModelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | model tests. 5 | 6 | You may define functions here to be used as helpers in 7 | your model tests. See `errors_on/2`'s definition as reference. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | using do 18 | quote do 19 | alias DemoChat.Repo 20 | import Ecto.Model 21 | import Ecto.Query, only: [from: 2] 22 | import DemoChat.ModelCase 23 | end 24 | end 25 | 26 | setup tags do 27 | unless tags[:async] do 28 | Ecto.Adapters.SQL.restart_test_transaction(DemoChat.Repo, []) 29 | end 30 | 31 | :ok 32 | end 33 | 34 | @doc """ 35 | Helper for returning list of errors in model when passed certain data. 36 | 37 | ## Examples 38 | 39 | Given a User model that lists `:name` as a required field and validates 40 | `:password` to be safe, it would return: 41 | 42 | iex> errors_on(%User{}, %{password: "password"}) 43 | [password: "is unsafe", name: "is blank"] 44 | 45 | You could then write your assertion like: 46 | 47 | assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"}) 48 | 49 | You can also create the changeset manually and retrieve the errors 50 | field directly: 51 | 52 | iex> changeset = User.changeset(%User{}, password: "password") 53 | iex> {:password, "is unsafe"} in changeset.errors 54 | true 55 | """ 56 | def errors_on(model, data) do 57 | model.__struct__.changeset(model, data).errors 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | 3 | Mix.Task.run "ecto.create", ["--quiet"] 4 | Mix.Task.run "ecto.migrate", ["--quiet"] 5 | Ecto.Adapters.SQL.begin_test_transaction(DemoChat.Repo) 6 | 7 | -------------------------------------------------------------------------------- /test/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DemoChat.ErrorViewTest do 2 | use DemoChat.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(DemoChat.ErrorView, "404.html", []) == 9 | "Page not found" 10 | end 11 | 12 | test "render 500.html" do 13 | assert render_to_string(DemoChat.ErrorView, "500.html", []) == 14 | "Server internal error" 15 | end 16 | 17 | test "render any other" do 18 | assert render_to_string(DemoChat.ErrorView, "505.html", []) == 19 | "Server internal error" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DemoChat.LayoutViewTest do 2 | use DemoChat.ConnCase, async: true 3 | end -------------------------------------------------------------------------------- /test/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DemoChat.PageViewTest do 2 | use DemoChat.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /web/channels/room_channel.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoChat.RoomChannel do 2 | use Phoenix.Channel 3 | 4 | def join("rooms:lobby", message, socket) do 5 | {:ok, socket} 6 | end 7 | 8 | def handle_in("new:message", msg, socket) do 9 | broadcast! socket, "new:message", %{user: msg["user"], body: msg["body"]} 10 | {:noreply, socket} 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoChat.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "rooms:*", DemoChat.RoomChannel 6 | 7 | ## Transports 8 | transport :websocket, Phoenix.Transports.WebSocket 9 | # transport :longpoll, Phoenix.Transports.LongPoll 10 | 11 | # Socket params are passed from the client and can 12 | # be used to verify and authenticate a user. After 13 | # verification, you can put default assigns into 14 | # the socket that will be set for all channels, ie 15 | # 16 | # {:ok, assign(socket, :user_id, verified_user_id)} 17 | # 18 | # To deny connection, return `:error`. 19 | # 20 | # See `Phoenix.Token` documentation for examples in 21 | # performing token verification on connect. 22 | def connect(_params, socket) do 23 | {:ok, socket} 24 | end 25 | 26 | # Socket id's are topics that allow you to identify all sockets for a given user: 27 | # 28 | # def id(socket), do: "users_socket:#{socket.assigns.user_id}" 29 | # 30 | # Would allow you to broadcast a "disconnect" event and terminate 31 | # all active sockets and channels for a given user: 32 | # 33 | # DemoChat.Endpoint.broadcast("users_socket:" <> user.id, "disconnect", %{}) 34 | # 35 | # Returning `nil` makes this socket anonymous. 36 | def id(_socket), do: nil 37 | 38 | channel "rooms:*", DemoChat.RoomChannel 39 | end 40 | -------------------------------------------------------------------------------- /web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoChat.PageController do 2 | use DemoChat.Web, :controller 3 | 4 | def index(conn, _params) do 5 | render conn, "index.html" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule DemoChat.Router do 2 | use DemoChat.Web, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_flash 8 | plug :protect_from_forgery 9 | plug :put_secure_browser_headers 10 | end 11 | 12 | pipeline :api do 13 | plug :accepts, ["json"] 14 | end 15 | 16 | scope "/", DemoChat do 17 | pipe_through :browser # Use the default browser stack 18 | 19 | get "/", PageController, :index 20 | end 21 | 22 | # Other scopes may use custom stacks. 23 | # scope "/api", DemoChat do 24 | # pipe_through :api 25 | # end 26 | end 27 | -------------------------------------------------------------------------------- /web/static/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amandasposito/demo_chat/da6ae6a59e2ca535f8471f9a0320a14700441255/web/static/assets/favicon.ico -------------------------------------------------------------------------------- /web/static/assets/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amandasposito/demo_chat/da6ae6a59e2ca535f8471f9a0320a14700441255/web/static/assets/images/phoenix.png -------------------------------------------------------------------------------- /web/static/assets/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /web/static/css/app.css: -------------------------------------------------------------------------------- 1 | * { margin:0px; padding:0px; } 2 | 3 | #messages{ 4 | min-height:300px; 5 | overflow-y: auto; 6 | border: solid 1px #ccc; 7 | border-radius:5px; 8 | margin:25px 0; 9 | } 10 | -------------------------------------------------------------------------------- /web/static/js/app.js: -------------------------------------------------------------------------------- 1 | // Brunch automatically concatenates all files in your 2 | // watched paths. Those paths can be configured at 3 | // config.paths.watched in "brunch-config.js". 4 | // 5 | // However, those files will only be executed if 6 | // explicitly imported. The only exception are files 7 | // in vendor, which are never wrapped in imports and 8 | // therefore are always executed. 9 | 10 | // Import dependencies 11 | // 12 | // If you no longer want to use a dependency, remember 13 | // to also remove its path from "config.paths.watched". 14 | import "deps/phoenix_html/web/static/js/phoenix_html" 15 | 16 | // Import local files 17 | // 18 | // Local files can be imported directly using relative 19 | // paths "./socket" or full ones "web/static/js/socket". 20 | 21 | import {Socket} from "deps/phoenix/web/static/js/phoenix" 22 | 23 | class App { 24 | static init() { 25 | var username = $("#username"); 26 | var msgBody = $("#message"); 27 | 28 | let socket = new Socket("/socket"); 29 | socket.connect(); 30 | socket.onClose( e => console.log("Closed connection") ); 31 | 32 | var channel = socket.channel("rooms:lobby", {}); 33 | 34 | channel.join() 35 | .receive( "error", () => console.log("Failed to connect") ) 36 | .receive( "ok", () => console.log("Connected") ) 37 | 38 | msgBody.off("keypress") 39 | .on("keypress", e => { 40 | if (e.keyCode == 13) { 41 | 42 | channel.push("new:message", { 43 | user: username.val(), 44 | body: msgBody.val() 45 | }); 46 | 47 | msgBody.val(""); 48 | } 49 | }); 50 | 51 | channel.on( "new:message", msg => this.renderMessage(msg) ) 52 | } 53 | 54 | static renderMessage(msg) { 55 | var messages = $("#messages") 56 | var user = this.sanitize(msg.user || "New User") 57 | var body = this.sanitize(msg.body) 58 | 59 | messages.append(`
[${user}]: ${body}
`) 60 | } 61 | 62 | static sanitize(str) { 63 | return $("").text(str).html() 64 | } 65 | } 66 | 67 | $( () => App.init() ) 68 | 69 | export default App 70 | -------------------------------------------------------------------------------- /web/static/js/socket.js: -------------------------------------------------------------------------------- 1 | // NOTE: The contents of this file will only be executed if 2 | // you uncomment its entry in "web/static/js/app.js". 3 | 4 | // To use Phoenix channels, the first step is to import Socket 5 | // and connect at the socket path in "lib/my_app/endpoint.ex": 6 | import {Socket} from "deps/phoenix/web/static/js/phoenix" 7 | 8 | let socket = new Socket("/socket", {params: {token: window.userToken}}) 9 | 10 | // When you connect, you'll often need to authenticate the client. 11 | // For example, imagine you have an authentication plug, `MyAuth`, 12 | // which authenticates the session and assigns a `:current_user`. 13 | // If the current user exists you can assign the user's token in 14 | // the connection for use in the layout. 15 | // 16 | // In your "web/router.ex": 17 | // 18 | // pipeline :browser do 19 | // ... 20 | // plug MyAuth 21 | // plug :put_user_token 22 | // end 23 | // 24 | // defp put_user_token(conn, _) do 25 | // if current_user = conn.assigns[:current_user] do 26 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id) 27 | // assign(conn, :user_token, token) 28 | // else 29 | // conn 30 | // end 31 | // end 32 | // 33 | // Now you need to pass this token to JavaScript. You can do so 34 | // inside a script tag in "web/templates/layout/app.html.eex": 35 | // 36 | // 37 | // 38 | // You will need to verify the user token in the "connect/2" function 39 | // in "web/channels/user_socket.ex": 40 | // 41 | // def connect(%{"token" => token}, socket) do 42 | // # max_age: 1209600 is equivalent to two weeks in seconds 43 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do 44 | // {:ok, user_id} -> 45 | // {:ok, assign(socket, :user, user_id)} 46 | // {:error, reason} -> 47 | // :error 48 | // end 49 | // end 50 | // 51 | // Finally, pass the token on connect as below. Or remove it 52 | // from connect if you don't care about authentication. 53 | 54 | socket.connect() 55 | 56 | // Now that you are connected, you can join channels with a topic: 57 | let channel = socket.channel("topic:subtopic", {}) 58 | channel.join() 59 | .receive("ok", resp => { console.log("Joined successfully", resp) }) 60 | .receive("error", resp => { console.log("Unable to join", resp) }) 61 | 62 | export default socket 63 | -------------------------------------------------------------------------------- /web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |