Skip to content

Commit 944a66b

Browse files
author
Tomáš Král
committed
channels and topics
1 parent 46ab6c0 commit 944a66b

File tree

14 files changed

+225
-98
lines changed

14 files changed

+225
-98
lines changed

assets/js/app.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@ import "../css/app.scss";
1414
//
1515
import "phoenix_html";
1616

17+
import socket from "./socket";
1718
import Player from "./player";
18-
let video = document.getElementById("video");
19-
if (video) {
20-
Player.init(video.id, video.getAttribute("data-player-id"), () => {
21-
console.log("player ready!");
22-
});
23-
}
19+
import Video from "./video";
20+
21+
Video.init(socket, document.getElementById("video"));

assets/js/socket.js

Lines changed: 8 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,10 @@
1-
// NOTE: The contents of this file will only be executed if
2-
// you uncomment its entry in "assets/js/app.js".
1+
import { Socket } from "phoenix";
32

4-
// To use Phoenix channels, the first step is to import Socket,
5-
// and connect at the socket path in "lib/web/endpoint.ex".
6-
//
7-
// Pass the token on params as below. Or remove it
8-
// from the params if you are not using authentication.
9-
import {Socket} from "phoenix"
3+
let socket = new Socket("/socket", {
4+
params: { token: window.userToken },
5+
logger: (kind, msg, data) => {
6+
console.log(`${kind}: ${msg}`, data);
7+
},
8+
});
109

11-
let socket = new Socket("/socket", {params: {token: window.userToken}})
12-
13-
// When you connect, you'll often need to authenticate the client.
14-
// For example, imagine you have an authentication plug, `MyAuth`,
15-
// which authenticates the session and assigns a `:current_user`.
16-
// If the current user exists you can assign the user's token in
17-
// the connection for use in the layout.
18-
//
19-
// In your "lib/web/router.ex":
20-
//
21-
// pipeline :browser do
22-
// ...
23-
// plug MyAuth
24-
// plug :put_user_token
25-
// end
26-
//
27-
// defp put_user_token(conn, _) do
28-
// if current_user = conn.assigns[:current_user] do
29-
// token = Phoenix.Token.sign(conn, "user socket", current_user.id)
30-
// assign(conn, :user_token, token)
31-
// else
32-
// conn
33-
// end
34-
// end
35-
//
36-
// Now you need to pass this token to JavaScript. You can do so
37-
// inside a script tag in "lib/web/templates/layout/app.html.eex":
38-
//
39-
// <script>window.userToken = "<%= assigns[:user_token] %>";</script>
40-
//
41-
// You will need to verify the user token in the "connect/3" function
42-
// in "lib/web/channels/user_socket.ex":
43-
//
44-
// def connect(%{"token" => token}, socket, _connect_info) do
45-
// # max_age: 1209600 is equivalent to two weeks in seconds
46-
// case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
47-
// {:ok, user_id} ->
48-
// {:ok, assign(socket, :user, user_id)}
49-
// {:error, reason} ->
50-
// :error
51-
// end
52-
// end
53-
//
54-
// Finally, connect to the socket:
55-
socket.connect()
56-
57-
// Now that you are connected, you can join channels with a topic:
58-
let channel = socket.channel("topic:subtopic", {})
59-
channel.join()
60-
.receive("ok", resp => { console.log("Joined successfully", resp) })
61-
.receive("error", resp => { console.log("Unable to join", resp) })
62-
63-
export default socket
10+
export default socket;

assets/js/video.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import Player from "./player";
2+
3+
let Video = {
4+
init(socket, element) {
5+
if (!element) {
6+
return;
7+
}
8+
let playerId = element.getAttribute("data-player-id");
9+
let videoId = element.getAttribute("data-id");
10+
socket.connect();
11+
Player.init(element.id, playerId, () => {
12+
this.onReady(videoId, socket);
13+
});
14+
},
15+
16+
onReady(videoId, socket) {
17+
let msgContainer = document.getElementById("msg-container");
18+
let msgInput = document.getElementById("msg-input");
19+
let postButton = document.getElementById("msg-submit");
20+
let vidChannel = socket.channel("videos:" + videoId);
21+
22+
postButton.addEventListener("click", (e) => {
23+
let payload = { body: msgInput.value, at: Player.getCurrentTime() };
24+
vidChannel
25+
.push("new_annotation", payload)
26+
.receive("error", (e) => console.log(e));
27+
msgInput.value = "";
28+
});
29+
30+
vidChannel.on("new_annotation", (resp) => {
31+
this.renderAnnotation(msgContainer, resp);
32+
});
33+
34+
vidChannel
35+
.join()
36+
.receive("ok", ({ annotations }) => {
37+
annotations.forEach((ann) => this.renderAnnotation(msgContainer, ann));
38+
})
39+
.receive("error", (reason) => console.log("join failed", reason));
40+
},
41+
42+
esc(str) {
43+
let div = document.createElement("div");
44+
div.appendChild(document.createTextNode(str));
45+
return div.innerHTML;
46+
},
47+
48+
renderAnnotation(msgContainer, { user, body, at }) {
49+
let template = document.createElement("div");
50+
51+
template.innerHTML = `
52+
<a href="#" data-seek="${this.esc(at)}">
53+
<b>${this.esc(user.username)}</b>: ${this.esc(body)}
54+
</a>
55+
`;
56+
msgContainer.appendChild(template);
57+
msgContainer.scrollTop = msgContainer.scrollHeight;
58+
},
59+
};
60+
61+
export default Video;

lib/rumbl/accounts.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ defmodule Rumbl.Accounts do
1515
Repo.get(User, id)
1616
end
1717

18+
def get_user!(id) do
19+
Repo.get(User, id)
20+
end
21+
1822
def get_user_by(params) do
1923
Repo.get_by(User, params)
2024
end

lib/rumbl/multimedia.ex

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule Rumbl.Multimedia do
99
alias Rumbl.Multimedia.Video
1010
alias Rumbl.Multimedia.Category
1111
alias Rumbl.Accounts
12+
alias Rumbl.Multimedia.Annotation
1213

1314
def list_videos do
1415
Video
@@ -65,6 +66,22 @@ defmodule Rumbl.Multimedia do
6566
Repo.get_by(Category, name: name) || Repo.insert!(%Category{name: name})
6667
end
6768

69+
def annotate_video(%Accounts.User{} = user, video_id, attrs) do
70+
%Annotation{video_id: video_id}
71+
|> Annotation.changeset(attrs)
72+
|> put_user(user)
73+
|> Repo.insert()
74+
end
75+
76+
def list_annotations(%Video{} = video) do
77+
Repo.all(
78+
from a in Ecto.assoc(video, :annotations),
79+
order_by: [asc: a.at, asc: a.id],
80+
limit: 500,
81+
preload: [:user]
82+
)
83+
end
84+
6885
defp put_user(changeset, user) do
6986
Ecto.Changeset.put_assoc(changeset, :user, user)
7087
end

lib/rumbl/multimedia/annotation.ex

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
defmodule Rumbl.Multimedia.Annotation do
2+
use Ecto.Schema
3+
import Ecto.Changeset
4+
5+
schema "anotations" do
6+
field :at, :integer
7+
field :body, :string
8+
9+
belongs_to :user, Rumbl.Accounts.User
10+
belongs_to :video, Rumbl.Multimedia.Video
11+
12+
timestamps()
13+
end
14+
15+
@doc false
16+
def changeset(annotation, attrs) do
17+
annotation
18+
|> cast(attrs, [:body, :at])
19+
|> validate_required([:body, :at])
20+
end
21+
end

lib/rumbl/multimedia/video.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ defmodule Rumbl.Multimedia.Video do
99
field :title, :string
1010
field :url, :string
1111
field :slug, :string
12+
1213
belongs_to :user, Rumbl.Accounts.User
1314
belongs_to :category, Rumbl.Multimedia.Category
15+
has_many :annotations, Rumbl.Multimedia.Annotation
1416

1517
timestamps()
1618
end

lib/rumbl_web/channels/user_socket.ex

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,24 @@ defmodule RumblWeb.UserSocket do
22
use Phoenix.Socket
33

44
## Channels
5-
# channel "room:*", RumblWeb.RoomChannel
5+
channel "videos:*", RumblWeb.VideoChannel
66

7-
# Socket params are passed from the client and can
8-
# be used to verify and authenticate a user. After
9-
# verification, you can put default assigns into
10-
# the socket that will be set for all channels, ie
11-
#
12-
# {:ok, assign(socket, :user_id, verified_user_id)}
13-
#
14-
# To deny connection, return `:error`.
15-
#
16-
# See `Phoenix.Token` documentation for examples in
17-
# performing token verification on connect.
18-
@impl true
19-
def connect(_params, socket, _connect_info) do
20-
{:ok, socket}
7+
@max_age 2 * 7 * 24 * 60 * 60
8+
def connect(%{"token" => token}, socket) do
9+
case Phoenix.Token.verify(
10+
socket,
11+
"user socket",
12+
token,
13+
max_age: @max_age
14+
) do
15+
{:ok, user_id} ->
16+
{:ok, assign(socket, :user_id, user_id)}
17+
18+
{:error, _reason} ->
19+
:error
20+
end
2121
end
2222

23-
# Socket id's are topics that allow you to identify all sockets for a given user:
24-
#
25-
# def id(socket), do: "user_socket:#{socket.assigns.user_id}"
26-
#
27-
# Would allow you to broadcast a "disconnect" event and terminate
28-
# all active sockets and channels for a given user:
29-
#
30-
# RumblWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
31-
#
32-
# Returning `nil` makes this socket anonymous.
33-
@impl true
34-
def id(_socket), do: nil
23+
def connect(_params, _socket), do: :error
24+
def id(socket), do: "users_socket:#{socket.assigns.user_id}"
3525
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
defmodule RumblWeb.VideoChannel do
2+
use RumblWeb, :channel
3+
alias Rumbl.{Accounts, Multimedia}
4+
alias RumblWeb.AnnotationView
5+
6+
def join("videos:" <> video_id, _params, socket) do
7+
video_id = String.to_integer(video_id)
8+
video = Multimedia.get_video!(video_id)
9+
10+
annotations =
11+
video
12+
|> Multimedia.list_annotations()
13+
|> Phoenix.View.render_many(AnnotationView, "annotation.json")
14+
15+
{:ok, %{annotations: annotations}, assign(socket, :video_id, video_id)}
16+
end
17+
18+
def handle_in(event, params, socket) do
19+
user = Accounts.get_user(socket.assigns.user_id)
20+
handle_in(event, params, user, socket)
21+
end
22+
23+
def handle_in("new_annotation", params, user, socket) do
24+
case Multimedia.annotate_video(user, socket.assigns.video_id, params) do
25+
{:ok, annotation} ->
26+
broadcast!(socket, "new_annotation", %{
27+
id: annotation.id,
28+
user: RumblWeb.UserView.render("user.json", %{user: user}),
29+
body: annotation.body,
30+
at: annotation.at
31+
})
32+
{:reply, :ok, socket}
33+
34+
{:error, changeset} ->
35+
{:reply, {:error, %{errors: changeset}}, socket}
36+
end
37+
end
38+
end

lib/rumbl_web/controllers/auth.ex

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,21 @@ defmodule RumblWeb.Auth do
1919
end
2020
def call(conn, _opts) do
2121
user_id = get_session(conn, :user_id)
22-
user = user_id && Accounts.get_user(user_id)
23-
assign(conn, :current_user, user)
22+
cond do
23+
user = conn.assigns[:current_user] ->
24+
put_current_user(conn, user)
25+
26+
user = user_id && Accounts.get_user!(user_id) ->
27+
put_current_user(conn, user)
28+
29+
true ->
30+
assign(conn, :current_user, nil)
31+
end
2432
end
2533

2634
def login(conn, user) do
2735
conn
28-
|> assign(:current_user, user)
36+
|> put_current_user(user)
2937
|> put_session(:user_id, user.id)
3038
|> configure_session(renew: true)
3139
end
@@ -41,4 +49,12 @@ defmodule RumblWeb.Auth do
4149
def logout(conn) do
4250
configure_session(conn, drop: true)
4351
end
52+
53+
defp put_current_user(conn, user) do
54+
token = Phoenix.Token.sign(conn, "user socket", user.id)
55+
56+
conn
57+
|> assign(:current_user, user)
58+
|> assign(:user_token, token)
59+
end
4460
end

lib/rumbl_web/templates/layout/app.html.eex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<title>Rumbl · Phoenix Framework</title>
88
<link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
99
<script defer type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
10+
<script>window.userToken="<%= assigns[:user_token] %>"</script>
1011
</head>
1112
<body>
1213
<header>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
defmodule RumblWeb.AnnotationView do
2+
use RumblWeb, :view
3+
4+
def render("annotation.json", %{annotation: annotation}) do
5+
%{
6+
id: annotation.id,
7+
body: annotation.body,
8+
at: annotation.at,
9+
user: render_one(annotation.user, RumblWeb.UserView, "user.json")
10+
}
11+
end
12+
end

lib/rumbl_web/views/user_view.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
defmodule RumblWeb.UserView do
22
use RumblWeb, :view
3-
43
alias Rumbl.Accounts
54

65

@@ -9,4 +8,8 @@ defmodule RumblWeb.UserView do
98
|> String.split(" ")
109
|> Enum.at(0)
1110
end
11+
12+
def render("user.json", %{user: user}) do
13+
%{id: user.id, username: user.username}
14+
end
1215
end

0 commit comments

Comments
 (0)