Page MenuHomePhorge

D351.1775484202.diff
No OneTemporary

Authored By
Unknown
Size
17 KB
Referenced Files
None
Subscribers
None

D351.1775484202.diff

diff --git a/lib/kolab_chat.ex b/lib/kolab_chat.ex
--- a/lib/kolab_chat.ex
+++ b/lib/kolab_chat.ex
@@ -12,6 +12,8 @@
supervisor(KolabChat.Repo, []),
# Start the endpoint when the application starts
supervisor(KolabChat.Endpoint, []),
+ # Start phoenix presence module
+ supervisor(KolabChat.Presence, []),
# Start your own worker by calling: KolabChat.Worker.start_link(arg1, arg2, arg3)
# worker(KolabChat.Worker, [arg1, arg2, arg3]),
]
diff --git a/web/channels/presence.ex b/web/channels/presence.ex
new file mode 100644
--- /dev/null
+++ b/web/channels/presence.ex
@@ -0,0 +1,77 @@
+defmodule KolabChat.Presence do
+ @moduledoc """
+ Provides presence tracking to channels and processes.
+
+ See the [`Phoenix.Presence`](http://hexdocs.pm/phoenix/Phoenix.Presence.html)
+ docs for more details.
+
+ ## Usage
+
+ Presences can be tracked in your channel after joining:
+
+ defmodule KolabChat.MyChannel do
+ use KolabChat.Web, :channel
+ alias KolabChat.Presence
+
+ def join("some:topic", _params, socket) do
+ send(self, :after_join)
+ {:ok, assign(socket, :user_id, ...)}
+ end
+
+ def handle_info(:after_join, socket) do
+ {:ok, _} = Presence.track(socket, socket.assigns.user_id, %{
+ online_at: inspect(System.system_time(:seconds))
+ })
+ push socket, "presence_state", Presence.list(socket)
+ {:noreply, socket}
+ end
+ end
+
+ In the example above, `Presence.track` is used to register this
+ channel's process as a presence for the socket's user ID, with
+ a map of metadata. Next, the current presence list for
+ the socket's topic is pushed to the client as a `"presence_state"` event.
+
+ Finally, a diff of presence join and leave events will be sent to the
+ client as they happen in real-time with the "presence_diff" event.
+ See `Phoenix.Presence.list/2` for details on the presence datastructure.
+
+ ## Fetching Presence Information
+
+ The `fetch/2` callback is triggered when using `list/1`
+ and serves as a mechanism to fetch presence information a single time,
+ before broadcasting the information to all channel subscribers.
+ This prevents N query problems and gives you a single place to group
+ isolated data fetching to extend presence metadata.
+
+ The function receives a topic and map of presences and must return a
+ map of data matching the Presence datastructure:
+
+ %{"123" => %{metas: [%{status: "away", phx_ref: ...}],
+ "456" => %{metas: [%{status: "online", phx_ref: ...}]}
+
+ The `:metas` key must be kept, but you can extend the map of information
+ to include any additional information. For example:
+
+ def fetch(_topic, entries) do
+ query =
+ from u in User,
+ where: u.id in ^Map.keys(entries),
+ select: {u.id, u}
+
+ users = query |> Repo.all |> Enum.into(%{})
+
+ for {key, %{metas: metas}} <- entries, into: %{} do
+ {key, %{metas: metas, user: users[key]}}
+ end
+ end
+
+ The function above fetches all users from the database who
+ have registered presences for the given topic. The fetched
+ information is then extended with a `:user` key of the user's
+ information, while maintaining the required `:metas` field from the
+ original presence data.
+ """
+ use Phoenix.Presence, otp_app: :kolab_chat,
+ pubsub_server: KolabChat.PubSub
+end
diff --git a/web/channels/system_channel.ex b/web/channels/system_channel.ex
new file mode 100644
--- /dev/null
+++ b/web/channels/system_channel.ex
@@ -0,0 +1,38 @@
+defmodule KolabChat.SystemChannel do
+ use KolabChat.Web, :channel
+
+ alias KolabChat.Presence
+
+ def join("system", _, socket) do
+ Process.flag(:trap_exit, true)
+ :timer.send_interval(10000, :ping)
+ send self(), :after_join
+
+ {:ok, socket}
+ end
+
+ def handle_info(:after_join, socket) do
+ Presence.track(socket, socket.assigns.user.username, %{
+ status: "online"
+ })
+
+ push socket, "info", %{user: socket.assigns.user.username}
+ push socket, "presence_state", Presence.list(socket)
+
+ {:noreply, socket}
+ end
+
+ def handle_info(:ping, socket) do
+ push socket, "new:msg", %{user: "SYSTEM", body: "ping"}
+
+ {:noreply, socket}
+ end
+
+ def handle_in("set-status", %{"status" => status}, socket) do
+ {:ok, _} = Presence.update(socket, socket.assigns.user.username, %{
+ status: status
+ })
+
+ {:noreply, socket}
+ end
+end
diff --git a/web/channels/user_socket.ex b/web/channels/user_socket.ex
--- a/web/channels/user_socket.ex
+++ b/web/channels/user_socket.ex
@@ -1,26 +1,31 @@
defmodule KolabChat.UserSocket do
use Phoenix.Socket
+ alias KolabChat.Repo
+ alias KolabChat.User
+
## Channels
- # channel "room:*", KolabChat.RoomChannel
+ #channel "room:*", KolabChat.RoomChannel
+ channel "system", KolabChat.SystemChannel
## Transports
transport :websocket, Phoenix.Transports.WebSocket
- # transport :longpoll, Phoenix.Transports.LongPoll
+ transport :longpoll, Phoenix.Transports.LongPoll
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into
# the socket that will be set for all channels, ie
- #
# {:ok, assign(socket, :user_id, verified_user_id)}
- #
# To deny connection, return `:error`.
- #
- # See `Phoenix.Token` documentation for examples in
- # performing token verification on connect.
- def connect(_params, socket) do
- {:ok, socket}
+ def connect(%{"token" => token}, socket) do
+ case Phoenix.Token.verify(socket, "user", token, max_age: 86400) do
+ {:ok, user_id} ->
+ socket = assign(socket, :user, Repo.get!(User, user_id))
+ {:ok, socket}
+ {:error, _} ->
+ :error
+ end
end
# Socket id's are topics that allow you to identify all sockets for a given user:
diff --git a/web/controllers/auth_controller.ex b/web/controllers/auth_controller.ex
--- a/web/controllers/auth_controller.ex
+++ b/web/controllers/auth_controller.ex
@@ -1,8 +1,6 @@
defmodule KolabChat.AuthController do
use KolabChat.Web, :controller
- alias KolabChat.User
-
@doc """
Handler for the default logon form
"""
diff --git a/web/static/css/app.css b/web/static/css/app.css
--- a/web/static/css/app.css
+++ b/web/static/css/app.css
@@ -11,3 +11,11 @@
display: block;
margin-bottom: 3px;
}
+
+ul.userlist li {
+ text-align: left;
+}
+
+ul.userlist li small {
+ color: #666;
+}
diff --git a/web/static/css/widgets.css b/web/static/css/widgets.css
new file mode 100644
--- /dev/null
+++ b/web/static/css/widgets.css
@@ -0,0 +1,13 @@
+/* Style for chat application widgets */
+
+.status-online .glyphicon {
+ color: green;
+}
+
+.status-away .glyphicon {
+ color: grey;
+}
+
+.status-busy .glyphicon {
+ color: red;
+}
diff --git a/web/static/js/api.js b/web/static/js/api.js
new file mode 100644
--- /dev/null
+++ b/web/static/js/api.js
@@ -0,0 +1,129 @@
+
+import {Socket, LongPoll, Presence} from "phoenix"
+import UserListWidget from "./widgets/userlist"
+import UserStatusWidget from "./widgets/userstatus"
+
+class KolabChat
+{
+ /**
+ * Configuration parameters:
+ * - userListElement: Id of HTML element where to put userslist widget
+ * - userStatusElement: Id of HTML element where to put users status widget
+ */
+ constructor(config)
+ {
+ this.config = config || {}
+ }
+
+ /**
+ * Initialize WebSocket communication
+ */
+ init(config)
+ {
+ if (config)
+ this.config = KolabChat.extend(this.config, config)
+
+ this.initWidgets()
+
+ // TODO: for integration with external systems we'll use configurable full wss:// url
+
+ this.socket = new Socket("/socket", {
+ params: {token: this.config.token},
+ logger: ((kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }),
+ })
+
+ this.socket.onOpen(e => {
+ // when connected start using 'system' channel
+ // for users' presence
+ this.initPresence()
+ })
+
+ this.socket.connect()
+ }
+
+ /**
+ * Initializes configured UI widgets
+ */
+ initWidgets()
+ {
+ if (this.config.userListElement) {
+ this.userListWidget = new UserListWidget(this.config.userListElement)
+ }
+
+ if (this.config.userStatusElement) {
+ let config = {statusChange: status => { this.setStatus(status) }}
+ this.userStatusWidget = new UserStatusWidget(this.config.userStatusElement, config)
+ }
+ }
+
+ /**
+ * Initialize user presence
+ * Create users list and status widgets
+ */
+ initPresence()
+ {
+ this.system = this.socket.channel("system")
+
+ this.system.on("info", info => {
+ this.username = info.user
+ })
+
+ this.system.on("presence_state", state => {
+ this.presences = Presence.syncState({}, state)
+ this.renderPresences(this.presences)
+ })
+
+ this.system.on("presence_diff", diff => {
+ // ignore initial presence_diff result, handle presence_state first
+ if (this.presences !== undefined) {
+ this.presences = Presence.syncDiff(this.presences, diff)
+ this.renderPresences(this.presences)
+ }
+ })
+
+ this.system.join()
+ }
+
+ /**
+ * Handler for presence responses
+ */
+ renderPresences(presences)
+ {
+ let userPresence
+
+ if (this.userStatusWidget && (userPresence = presences[this.username])) {
+ userPresence = this.listBy(this.username, userPresence)
+ this.userStatusWidget.render(userPresence)
+ }
+
+ if (this.userListWidget) {
+ presences = Presence.list(presences, this.listBy)
+ this.userListWidget.render(presences)
+ }
+ }
+
+ listBy(user, {metas: metas})
+ {
+ return {
+ user: user,
+ status: metas[0].status
+ }
+ }
+
+ /**
+ * User status change
+ */
+ setStatus(status)
+ {
+ this.system.push('set-status', {status: status});
+ }
+
+
+ static extend(obj, src)
+ {
+ Object.keys(src).forEach(function(key) { obj[key] = src[key] })
+ return obj
+ }
+}
+
+export default KolabChat
diff --git a/web/static/js/app.js b/web/static/js/app.js
--- a/web/static/js/app.js
+++ b/web/static/js/app.js
@@ -18,4 +18,15 @@
// Local files can be imported directly using relative
// paths "./socket" or full ones "web/static/js/socket".
-// import socket from "./socket"
+import KolabChat from "./api"
+
+// Initialize KolabChat API object
+let chat = new KolabChat({
+ userListElement: "userlist",
+ userStatusElement: "userstatus",
+})
+
+// If user is authenticated start the app
+if (window.userToken) {
+ chat.init({token: window.userToken})
+}
diff --git a/web/static/js/socket.js b/web/static/js/socket.js
deleted file mode 100644
--- a/web/static/js/socket.js
+++ /dev/null
@@ -1,62 +0,0 @@
-// NOTE: The contents of this file will only be executed if
-// you uncomment its entry in "web/static/js/app.js".
-
-// To use Phoenix channels, the first step is to import Socket
-// and connect at the socket path in "lib/my_app/endpoint.ex":
-import {Socket} from "phoenix"
-
-let socket = new Socket("/socket", {params: {token: window.userToken}})
-
-// When you connect, you'll often need to authenticate the client.
-// For example, imagine you have an authentication plug, `MyAuth`,
-// which authenticates the session and assigns a `:current_user`.
-// If the current user exists you can assign the user's token in
-// the connection for use in the layout.
-//
-// In your "web/router.ex":
-//
-// pipeline :browser do
-// ...
-// plug MyAuth
-// plug :put_user_token
-// end
-//
-// defp put_user_token(conn, _) do
-// if current_user = conn.assigns[:current_user] do
-// token = Phoenix.Token.sign(conn, "user socket", current_user.id)
-// assign(conn, :user_token, token)
-// else
-// conn
-// end
-// end
-//
-// Now you need to pass this token to JavaScript. You can do so
-// inside a script tag in "web/templates/layout/app.html.eex":
-//
-// <script>window.userToken = "<%= assigns[:user_token] %>";</script>
-//
-// You will need to verify the user token in the "connect/2" function
-// in "web/channels/user_socket.ex":
-//
-// def connect(%{"token" => token}, socket) do
-// # max_age: 1209600 is equivalent to two weeks in seconds
-// case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
-// {:ok, user_id} ->
-// {:ok, assign(socket, :user, user_id)}
-// {:error, reason} ->
-// :error
-// end
-// end
-//
-// Finally, pass the token on connect as below. Or remove it
-// from connect if you don't care about authentication.
-
-socket.connect()
-
-// Now that you are connected, you can join channels with a topic:
-let channel = socket.channel("topic:subtopic", {})
-channel.join()
- .receive("ok", resp => { console.log("Joined successfully", resp) })
- .receive("error", resp => { console.log("Unable to join", resp) })
-
-export default socket
diff --git a/web/static/js/widgets/userlist.js b/web/static/js/widgets/userlist.js
new file mode 100644
--- /dev/null
+++ b/web/static/js/widgets/userlist.js
@@ -0,0 +1,26 @@
+
+class UserListWidget
+{
+ constructor(id, config)
+ {
+ this.config = config || {}
+ this.id = id
+ }
+
+ /**
+ * Render users list
+ */
+ render(presences)
+ {
+ let userListElement = document.getElementById(this.id)
+
+ userListElement.innerHTML = presences.map(presence => `
+ <li class="list-group-item status-${presence.status}">
+ <span class="glyphicon glyphicon-user"></span> ${presence.user}
+ </li>
+ `)
+ .join("")
+ }
+}
+
+export default UserListWidget
diff --git a/web/static/js/widgets/userstatus.js b/web/static/js/widgets/userstatus.js
new file mode 100644
--- /dev/null
+++ b/web/static/js/widgets/userstatus.js
@@ -0,0 +1,47 @@
+
+class UserStatusWidget
+{
+ /**
+ * Configuration:
+ * - statusChange: handler function for status change
+ */
+ constructor(id, config)
+ {
+ this.config = config || {}
+ this.id = id
+ }
+
+ /**
+ * Renders user status widget
+ */
+ render(presence)
+ {
+ let userStatusElement = document.getElementById(this.id)
+ let icon = '<span class="glyphicon glyphicon-user"></span>'
+
+ userStatusElement.innerHTML = `
+ <div class="dropdown">
+ <button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
+ <span class="status-${presence.status}">
+ ${icon} ${presence.user}
+ </span>
+ <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
+ <li><a href="#" class="status-online">${icon} Online</a></li>
+ <li><a href="#" class="status-away">${icon} Away</a></li>
+ <li><a href="#" class="status-busy">${icon} Busy</a></li>
+ </ul>
+ </div>
+ `
+
+ $('.dropdown-menu > li', userStatusElement).click(e => {
+ let status_class = $(e.target).attr('class')
+ if (this.config.statusChange && !$('button > span:first', userStatusElement).hasClass(status_class)) {
+ this.config.statusChange(status_class.replace(/^status-/, ''))
+ }
+ })
+ }
+}
+
+export default UserStatusWidget
diff --git a/web/templates/layout/app.html.eex b/web/templates/layout/app.html.eex
--- a/web/templates/layout/app.html.eex
+++ b/web/templates/layout/app.html.eex
@@ -18,8 +18,10 @@
<nav role="navigation">
<ul class="nav nav-pills pull-right">
<li><%= link gettext("Logout"), to: "/auth/logout" %></li>
+ <li id="userstatus"></li>
</ul>
</nav>
+ <script>window.userToken = "<%= Phoenix.Token.sign(@conn, "user", @conn.assigns[:user].id) %>"</script>
<% else %>
<%= form_for @conn, "/auth/default/callback", [as: :logon], fn f -> %>
<%= text_input f, :username, placeholder: gettext("Username") %>
@@ -38,6 +40,8 @@
</main>
</div> <!-- /container -->
+ <script src="<%= static_path(@conn, "/js/jquery.min.js") %>"></script>
+ <script src="<%= static_path(@conn, "/js/bootstrap.min.js") %>"></script>
<script src="<%= static_path(@conn, "/js/app.js") %>"></script>
</body>
</html>
diff --git a/web/templates/page/index.html.eex b/web/templates/page/index.html.eex
--- a/web/templates/page/index.html.eex
+++ b/web/templates/page/index.html.eex
@@ -1,4 +1,9 @@
<div class="jumbotron">
- <h2><%= gettext "Welcome to %{name}", name: "Kolab Chat!" %></h2>
- <p class="lead"><%= gettext "Real-time communication for the Kolab groupware system." %></p>
+ <%= if @conn.assigns[:user] do %>
+ Users List:
+ <ul id="userlist" class="userlist list-group"></ul>
+ <% else %>
+ <h2><%= gettext "Welcome to %{name}", name: "Kolab Chat!" %></h2>
+ <p class="lead"><%= gettext "Real-time communication for the Kolab groupware system." %></p>
+ <% end %>
</div>
diff --git a/web/web.ex b/web/web.ex
--- a/web/web.ex
+++ b/web/web.ex
@@ -31,6 +31,8 @@
use Phoenix.Controller
alias KolabChat.Repo
+ alias KolabChat.User
+
import Ecto
import Ecto.Query

File Metadata

Mime Type
text/plain
Expires
Mon, Apr 6, 2:03 PM (8 h, 8 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18831263
Default Alt Text
D351.1775484202.diff (17 KB)

Event Timeline