Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117945359
D351.1775483204.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
26 KB
Referenced Files
None
Subscribers
None
D351.1775483204.diff
View Options
diff --git a/brunch-config.js b/brunch-config.js
--- a/brunch-config.js
+++ b/brunch-config.js
@@ -54,6 +54,9 @@
babel: {
// Do not use ES6 compiler in vendor code
ignore: [/web\/static\/vendor/]
+ },
+ copycat: {
+ fonts: ["node_modules/bootstrap/fonts"]
}
},
@@ -64,6 +67,11 @@
},
npm: {
- enabled: true
+ enabled: true,
+ globals: {
+ $: "jquery",
+ jQuery: "jquery",
+ bootstrap: "bootstrap"
+ }
}
};
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/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -7,7 +7,9 @@
},
"dependencies": {
"phoenix": "file:deps/phoenix",
- "phoenix_html": "file:deps/phoenix_html"
+ "phoenix_html": "file:deps/phoenix_html",
+ "jquery": ">=2.1",
+ "bootstrap": "~3.3.7"
},
"devDependencies": {
"babel-brunch": "~6.0.0",
@@ -15,6 +17,7 @@
"clean-css-brunch": "~2.0.0",
"css-brunch": "~2.0.0",
"javascript-brunch": "~2.0.0",
- "uglify-js-brunch": "~2.0.1"
+ "uglify-js-brunch": "~2.0.1",
+ "copycat-brunch": "~1.1.0"
}
}
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/room_channel.ex b/web/channels/room_channel.ex
new file mode 100644
--- /dev/null
+++ b/web/channels/room_channel.ex
@@ -0,0 +1,12 @@
+defmodule KolabChat.RoomChannel do
+ use KolabChat.Web, :channel
+
+ def join("room:lobby", _, socket) do
+ {:ok, socket}
+ end
+
+ def handle_in("new:message", message, socket) do
+ broadcast! socket, "new:message", %{user: message["user"], body: message["body"]}
+ {:noreply, socket}
+ end
+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/controllers/chat_controller.ex b/web/controllers/chat_controller.ex
new file mode 100644
--- /dev/null
+++ b/web/controllers/chat_controller.ex
@@ -0,0 +1,11 @@
+defmodule KolabChat.ChatController do
+ use KolabChat.Web, :controller
+
+ plug :put_layout, "chat.html"
+
+ def index(conn, %{"room" => room} = _params) do
+ conn
+ |> assign(:room, room)
+ |> render("index.html")
+ end
+end
diff --git a/web/controllers/plugs/set_user.ex b/web/controllers/plugs/set_user.ex
--- a/web/controllers/plugs/set_user.ex
+++ b/web/controllers/plugs/set_user.ex
@@ -6,6 +6,17 @@
def init(params), do: params
+ # token authentication
+ def call(%{"params": %{"token" => token}} = conn, _params) do
+ case Phoenix.Token.verify(conn, "user", token, max_age: 86400) do
+ {:ok, user_id} ->
+ assign(conn, :user, Repo.get!(User, user_id))
+ _ ->
+ assign(conn, :user, nil)
+ end
+ end
+
+ # session authentication
def call(conn, _params) do
user_id = get_session(conn, :user_id)
diff --git a/web/router.ex b/web/router.ex
--- a/web/router.ex
+++ b/web/router.ex
@@ -16,11 +16,18 @@
end
scope "/", KolabChat do
- pipe_through :browser # Use the default browser stack
+ pipe_through :browser
get "/", PageController, :index
end
+ scope "/chat", KolabChat do
+ pipe_through :browser
+
+ get "/", ChatController, :index
+ get "/:room", ChatController, :index
+ end
+
scope "/auth", KolabChat do
pipe_through :browser
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,23 @@
display: block;
margin-bottom: 3px;
}
+
+ul.userlist li {
+ text-align: left;
+}
+
+ul.userlist li small {
+ color: #666;
+}
+
+#chat_txt {
+ height: 300px;
+ width: 100%;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ margin-bottom: 5px;
+}
+
+#chat_txt_input {
+ width: 100%;
+}
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,26 @@
+/* Style for chat application widgets */
+
+.status-online .glyphicon-user {
+ color: green;
+}
+
+.status-away .glyphicon-user {
+ color: grey;
+}
+
+.status-busy .glyphicon-user {
+ color: red;
+}
+
+.userlist .btn-group {
+ float: right;
+}
+
+.textchat {
+ overflow-x: hidden;
+ overflow-y: scroll;
+}
+
+.textchat p {
+ margin: 0;
+}
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,198 @@
+
+import {Socket, LongPoll, Presence} from "phoenix"
+import UserListWidget from "./widgets/userlist"
+import UserStatusWidget from "./widgets/userstatus"
+import ChatInputWidget from "./widgets/chatinput"
+import ChatRoomWidget from "./widgets/chatroom"
+
+class KolabChat
+{
+ /**
+ * Configuration parameters:
+ * - token: User session token
+ * - roomId: Chat room Id to join in
+ * - userListElement: Id of HTML element where to put userslist widget
+ * - userStatusElement: Id of HTML element where to put users status widget
+ * - chatInputElement: Id of HTML element which is a text chat input
+ * - chatRoomElement: Id of HTML element where to put text conversation
+ */
+ 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()
+ if (this.config.roomId) {
+ this.initRoom(this.config.roomId)
+ }
+ })
+
+ this.socket.connect()
+ }
+
+ /**
+ * Initializes configured UI widgets
+ */
+ initWidgets()
+ {
+ let config
+
+ if (this.config.userListElement && $('#' + this.config.userListElement).length) {
+ config = {
+ username: this.username,
+ openChat: (e, user) => { this.openChat(e, user) }
+ }
+ this.userListWidget = new UserListWidget(this.config.userListElement, config)
+ }
+
+ if (this.config.userStatusElement && $('#' + this.config.userStatusElement).length) {
+ config = {
+ username: this.username,
+ statusChange: status => { this.setStatus(status) }
+ }
+ this.userStatusWidget = new UserStatusWidget(this.config.userStatusElement, config)
+ }
+
+ if (this.config.chatRoomElement && $('#' + this.config.chatRoomElement).length) {
+ this.chatRoomWidget = new ChatRoomWidget(this.config.chatRoomElement)
+ }
+
+ if (this.config.chatInputElement && $('#' + this.config.chatInputElement).length) {
+ config = {
+ submit: (e, msg) => { this.sendTxtMessage(e, msg) }
+ }
+ this.chatInputWidget = new ChatInputWidget(this.config.chatInputElement, 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()
+ }
+
+ /**
+ * Initialize chat channel
+ */
+ initRoom(roomId)
+ {
+ this.chat = this.socket.channel("room:" + roomId)
+
+ this.chat.on("new:message", message => {
+ this.chatRoomWidget.append(message.user, message.body)
+ })
+
+ this.chat.join()
+ }
+
+ /**
+ * Send text message to the chat room
+ */
+ sendTxtMessage(event, message)
+ {
+ this.chat.push("new:message", {
+ user: this.username, // TODO: this is not really needed
+ body: message
+ })
+ }
+
+ /**
+ * 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});
+ }
+
+ /**
+ * Open chat window (and create a new chat room)
+ */
+ openChat(event, user)
+ {
+ // TODO: Use 'system' channel to create a chat room first
+ let roomId = "lobby"
+
+ let windowName = 'KolabChat' + new Date().getTime()
+ let url = "/chat/" + encodeURIComponent(roomId)
+ + "/?token=" + encodeURIComponent(this.config.token)
+
+ var extwin = window.open(url, windowName);
+ }
+
+
+ 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,20 @@
// 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",
+ chatRoomElement: "chat_txt",
+ chatInputElement: "chat_txt_input"
+})
+
+// If user is authenticated start the app
+if (window.userToken) {
+ chat.init({
+ token: window.userToken,
+ roomId: window.roomId
+ })
+}
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/chatinput.js b/web/static/js/widgets/chatinput.js
new file mode 100644
--- /dev/null
+++ b/web/static/js/widgets/chatinput.js
@@ -0,0 +1,36 @@
+
+class ChatInputWidget
+{
+ /**
+ * Configuration:
+ * - submit: handler function for submit the text to a chat room
+ */
+ constructor(id, config)
+ {
+ this.config = config || {}
+ this.id = id
+ this.render()
+ }
+
+ /**
+ * Renders text chat input widget
+ */
+ render()
+ {
+ let icon = '<span class="glyphicon glyphicon-comment"></span>'
+ let html = `<span class="input-group-addon">${icon}</span>
+ <input id="chat_txt_input" name="txt_input" value="" class="form-control" autofocus=true />`
+
+ $('#' + this.id)
+ .html(html)
+ .on('keypress', 'input', e => {
+ let txt
+ if (e.keyCode == 13 && this.config.submit && (txt = $(e.target).val())) {
+ this.config.submit(e, txt)
+ $(e.target).val('')
+ }
+ })
+ }
+}
+
+export default ChatInputWidget
diff --git a/web/static/js/widgets/chatroom.js b/web/static/js/widgets/chatroom.js
new file mode 100644
--- /dev/null
+++ b/web/static/js/widgets/chatroom.js
@@ -0,0 +1,35 @@
+
+class ChatRoomWidget
+{
+ constructor(id, config)
+ {
+ this.config = config || {}
+ this.id = id
+ this.render()
+ }
+
+ /**
+ * Renders text chat room widget
+ */
+ render()
+ {
+ }
+
+ /**
+ * Appends text message to the chat room widget
+ */
+ append(user, text)
+ {
+ user = ChatRoomWidget.sanitize(user)
+ text = ChatRoomWidget.sanitize(text)
+
+ $("#" + this.id).append(`<p><b>[${user}]</b>: ${text}</p>`)
+ }
+
+ static sanitize(str)
+ {
+ return $("<div/>").text(str).html()
+ }
+}
+
+export default ChatRoomWidget
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,62 @@
+
+class UserListWidget
+{
+ /**
+ * Configuration:
+ * - username: Current user name
+ * - openChat: callback for "Open chat" button
+ */
+ constructor(id, config)
+ {
+ this.config = config || {}
+ this.id = id
+ }
+
+ /**
+ * Render users list
+ */
+ render(presences)
+ {
+ let list = $('#' + this.id)
+ let config = this.config
+ let html = presences.map(presence => {
+ let buttons = this.buttons(presence)
+ return `
+ <li class="list-group-item status-${presence.status}" data-user="${presence.user}">
+ <span class="glyphicon glyphicon-user"></span> ${presence.user}
+ ${buttons}
+ </li>`
+ })
+ .join("")
+
+ list.html(html)
+
+ $('button', list).on('click', function(e) {
+ let action = $(this).data('action')
+ if (action && config[action]) {
+ config[action](e, $(this).parents('li').data('user'))
+ }
+ })
+ }
+
+ /**
+ * Render users list record buttons
+ */
+ buttons(presence)
+ {
+ let buttons = ''
+
+ if (this.config.openChat) { // && presence.user != this.config.username) {
+ let btn_name = '<span class="glyphicon glyphicon-comment"></span> Open chat'
+ buttons += `<button type="button" class="btn btn-primary btn-xs" data-action="openChat">${btn_name}</button>`
+ }
+
+ if (buttons) {
+ buttons = '<div class="btn-group">' + buttons + '</div>'
+ }
+
+ return buttons
+ }
+}
+
+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/chat/index.html.eex b/web/templates/chat/index.html.eex
new file mode 100644
--- /dev/null
+++ b/web/templates/chat/index.html.eex
@@ -0,0 +1,2 @@
+<div id="chat_txt" class="textchat"></div>
+<div id="chat_txt_input" class="textchatinput input-group"></div>
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") %>
diff --git a/web/templates/layout/chat.html.eex b/web/templates/layout/chat.html.eex
new file mode 100644
--- /dev/null
+++ b/web/templates/layout/chat.html.eex
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta name="description" content="">
+ <meta name="author" content="">
+
+ <title><%= gettext "Kolab Real Time Communication" %></title>
+ <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
+ </head>
+
+ <body>
+ <div class="container chat">
+ <main role="main">
+ <%= render @view_module, @view_template, assigns %>
+ </main>
+ </div> <!-- /container -->
+ <%= if @conn.assigns[:user] do %>
+ <script>window.userToken = "<%= Phoenix.Token.sign(@conn, "user", @conn.assigns[:user].id) %>"</script>
+ <script>window.roomId = "<%= @conn.assigns[:room] %>"</script>
+ <% end %>
+ <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/views/chat_view.ex b/web/views/chat_view.ex
new file mode 100644
--- /dev/null
+++ b/web/views/chat_view.ex
@@ -0,0 +1,3 @@
+defmodule KolabChat.ChatView do
+ use KolabChat.Web, :view
+end
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
Details
Attached
Mime Type
text/plain
Expires
Mon, Apr 6, 1:46 PM (6 h, 29 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18827284
Default Alt Text
D351.1775483204.diff (26 KB)
Attached To
Mode
D351: Websockets, auth tokens, presence, text chat
Attached
Detach File
Event Timeline