')
$('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) {
- let btn_name = ' Open chat'
+ let btn_name = ' Open chat' // TODO: Localization
buttons += ``
}
if (buttons) {
buttons = '
' + buttons + '
'
}
return buttons
}
}
export default UserListWidget
diff --git a/assets/js/widgets/userstatus.js b/assets/js/widgets/userstatus.js
index c1437af..2413092 100644
--- a/assets/js/widgets/userstatus.js
+++ b/assets/js/widgets/userstatus.js
@@ -1,63 +1,65 @@
class UserStatusWidget
{
/**
* Configuration:
* - statusChange: handler function for status change
*/
constructor(id, config)
{
this.config = config || {}
this.id = id
// FIXME: should we get that list from the backend?
this.status_list = [
// user is available for chat
"online",
"away",
// user is connected and visible, but not available
"busy",
"unavailable",
// user is shown as offline
"invisible",
"offline"
];
+
+ // TODO: render() with defalt state here
}
/**
* Renders user status widget
*/
render(presence)
{
let userStatusElement = document.getElementById(this.id)
let icon = ''
let options = list => {
return $.map(list, status =>
`
`
$('.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/lib/kolab_chat.ex b/lib/kolab_chat.ex
index 26d6447..792bfd9 100644
--- a/lib/kolab_chat.ex
+++ b/lib/kolab_chat.ex
@@ -1,24 +1,26 @@
defmodule KolabChat do
use Application
# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
import Supervisor.Spec
# Define workers and child supervisors to be supervised
children = [
# Start the endpoint when the application starts
supervisor(KolabChat.Web.Endpoint, []),
# Start phoenix presence module
supervisor(KolabChat.Web.Presence, []),
+ # Start Rooms database
+ worker(KolabChat.Rooms, []),
# Start your own worker by calling: KolabChat.Worker.start_link(arg1, arg2, arg3)
# worker(KolabChat.Worker, [arg1, arg2, arg3]),
]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: KolabChat.Supervisor]
Supervisor.start_link(children, opts)
end
end
diff --git a/lib/kolab_chat/rooms.ex b/lib/kolab_chat/rooms.ex
new file mode 100644
index 0000000..9827612
--- /dev/null
+++ b/lib/kolab_chat/rooms.ex
@@ -0,0 +1,59 @@
+defmodule KolabChat.Rooms do
+ use GenServer
+
+ def get(roomId) do
+ GenServer.call(__MODULE__, {:get, roomId})
+ end
+
+ def find(room) do
+ GenServer.call(__MODULE__, {:find, room})
+ end
+
+ def set(roomId, room) do
+ GenServer.call(__MODULE__, {:set, roomId, room})
+ end
+
+ def start_link do
+ :ets.new(:rooms, [:set, :named_table, :public])
+ GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
+ end
+
+ def handle_call({:get, roomId}, _from, state) do
+ room =
+ case lookup_rooms(roomId) do
+ nil -> nil
+ room -> Map.put(room, :id, roomId)
+ end
+
+ {:reply, room, state}
+ end
+
+ # Finding a room by any attribute value, supported: id, name
+ def handle_call({:find, room}, _from, state) do
+ has_id = Map.has_key?(room, :id)
+ room =
+ case has_id and lookup_rooms(room.id) do
+ # TODO: searching by name alias
+ nil -> nil
+ metadata -> Map.merge(room, metadata)
+ end
+
+ {:reply, room, state}
+ end
+
+ def handle_call({:set, roomId, metadata}, _from, state) do
+ metadata = lookup_rooms(roomId, %{})
+ |> Map.merge(metadata)
+ |> Map.delete(:id)
+
+ :ets.insert(:rooms, {roomId, metadata})
+ {:reply, :ok, state}
+ end
+
+ defp lookup_rooms(roomId, default \\ nil) do
+ case :ets.lookup(:rooms, roomId) do
+ [{_, room}] -> room
+ _ -> default
+ end
+ end
+end
diff --git a/lib/kolab_chat/web/channels/presence.ex b/lib/kolab_chat/web/channels/presence.ex
index d51fceb..b0ff3b8 100644
--- a/lib/kolab_chat/web/channels/presence.ex
+++ b/lib/kolab_chat/web/channels/presence.ex
@@ -1,77 +1,156 @@
defmodule KolabChat.Web.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.Web.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
+
+ require Amnesia
+ require Amnesia.Helper
+
+ alias KolabChat.Database
+
+ @status [
+ # user is available for chat
+ :online,
+ :away,
+ # user is connected and visible, but not available
+ :busy,
+ :unavailable,
+ # user is shown as offline
+ :invisible,
+ :offline
+ ]
+
+ def track_presence(socket) do
+ track(socket, socket.assigns.user.id, %{
+ username: socket.assigns.user.username,
+ status: get_status(socket),
+ context: socket.assigns.context
+ })
+ end
+
+ def update_status(socket, status) do
+ case check_status(status) do
+ :invalid ->
+ socket
+ status ->
+ update(socket, socket.assigns.user.id, %{
+ username: socket.assigns.user.username,
+ status: status,
+ context: socket.assigns.context
+ })
+ socket
+ end
+ end
+ # Get the last user/context status from the database
+ def get_status(socket) do
+ key = Integer.to_string(socket.assigns.user.id) <> ":" <> socket.assigns.context
+
+ Amnesia.transaction do
+ case Database.Status.read(key) do
+ # use last status
+ %Database.Status{status: status} -> status
+ # otherwise set status to online
+ _ -> :online
+ end
+ end
+ end
+
+ # Save the current user/context status to the database
+ def set_status(socket, status) do
+ case check_status(status) do
+ :invalid ->
+ socket
+ status ->
+ key = Integer.to_string(socket.assigns.user.id) <> ":" <> socket.assigns.context
+ Amnesia.transaction do
+ Database.Status.write(%Database.Status{key: key, status: status})
+ end
+ socket
+ end
+ end
+
+ # Makes sure the provided status name is supported
+ # Returns status name as an atom
+ defp check_status(status) do
+ status = String.to_atom(status)
+
+ if Enum.member?(@status, status) do
+ status
+ else
+ :invalid
+ end
+ end
+
end
diff --git a/lib/kolab_chat/web/channels/room_channel.ex b/lib/kolab_chat/web/channels/room_channel.ex
index 3987328..7b67a61 100644
--- a/lib/kolab_chat/web/channels/room_channel.ex
+++ b/lib/kolab_chat/web/channels/room_channel.ex
@@ -1,30 +1,52 @@
defmodule KolabChat.Web.RoomChannel do
use KolabChat.Web, :channel
alias KolabChat.Web.Endpoint
+ alias KolabChat.Rooms
@spec join(topic :: binary(), args :: map(), socket :: pid()) :: {:ok, socket :: pid()}
- def join("room:" <> room_name, _, socket) do
+ def join("room:" <> room_name, args, socket) do
+ room = Rooms.find(%{:id => room_name, :name => room_name})
+ socket = socket
+ |> assign(:room, room)
+ |> assign(:context, get_context(args))
+
+ send self(), :after_join
{:ok, socket}
end
@spec handle_in(topic :: binary, args :: map(), socket :: pid()) :: {:noreply, socket :: pid()}
def handle_in("new:message", message, socket) do
broadcast! socket, "new:message", %{user: socket.assigns.user.username, body: message["body"]}
{:noreply, socket}
end
def handle_in("ctl:invite", %{"users" => invitee}, socket) when is_bitstring(invitee) do
Endpoint.broadcast!("user:" <> invitee, "notify:invite", %{ user: socket.assigns.user.username, room: socket.topic})
{:noreply, socket}
end
def handle_in("ctl:invite", %{"users" => invitees}, socket) when is_list(invitees) do
Enum.each(invitees, fn(invitee) -> Endpoint.broadcast!("user:" <> invitee, "notify:invite", %{ user: socket.assigns.user.username, room: socket.topic}) end)
{:noreply, socket}
end
def handle_in("ctl:invite", params, socket) do
{:noreply, socket}
end
+
+ @spec handle_info(:after_join, socket :: pid()) :: {:noreply, socket :: pid()}
+ def handle_info(:after_join, socket) do
+ push socket, "info", socket.assigns.room
+ # Presence.track_presence(socket)
+ {:noreply, socket}
+ end
+
+ defp get_context(args) do
+ case args do
+ %{:context => context} -> context
+ _ -> "default"
+ end
+ end
+
end
diff --git a/lib/kolab_chat/web/channels/system_channel.ex b/lib/kolab_chat/web/channels/system_channel.ex
index 76460b6..bd7909b 100644
--- a/lib/kolab_chat/web/channels/system_channel.ex
+++ b/lib/kolab_chat/web/channels/system_channel.ex
@@ -1,110 +1,42 @@
defmodule KolabChat.Web.SystemChannel do
use KolabChat.Web, :channel
- @status [
- # user is available for chat
- :online,
- :away,
- # user is connected and visible, but not available
- :busy,
- :unavailable,
- # user is shown as offline
- :invisible,
- :offline
- ]
-
@spec join(topic :: binary(), args :: map(), socket :: pid()) :: {:ok, socket :: pid()}
- def join("system", %{"context" => context}, socket) do
- perform_join(context, socket)
- end
-
- def join("system", _args, socket) do
- perform_join("default", socket)
+ def join("system", args, socket) do
+ perform_join(get_context(args), socket)
end
@spec handle_info(:after_join, socket :: pid()) :: {:noreply, socket :: pid()}
def handle_info(:after_join, socket) do
push socket, "presence_state", Presence.list(socket)
push socket, "info", %{user: socket.assigns.user.username, userId: socket.assigns.user.id}
- Presence.track(socket, socket.assigns.user.id, %{
- username: socket.assigns.user.username,
- status: get_user_status(socket),
- context: socket.assigns.context
- })
+ Presence.track_presence(socket)
{:noreply, socket}
end
@spec handle_in(topic :: binary, args :: map(), socket :: pid()) :: {:noreply, socket :: pid()}
def handle_in("set-status", %{"status" => status}, socket) do
- status = check_status(status)
socket
- |> update_presence_status(status)
- |> set_user_status(status)
+ |> Presence.update_status(status)
+ |> Presence.set_status(status)
{:noreply, socket}
end
defp perform_join(context, socket) do
socket = assign(socket, :context, context)
send self(), :after_join
{:ok, socket}
end
- defp update_presence_status(socket, :invalid), do: socket
- defp update_presence_status(socket, status) do
- Presence.update(socket, socket.assigns.user.id, %{
- username: socket.assigns.user.username,
- status: status,
- context: socket.assigns.context
- })
-
- socket
- end
-
- # Makes sure the provided status name is supported
- # Returns status name as an atom
- defp check_status(status) do
- status = String.to_atom(status)
-
- if Enum.member?(@status, status) do
- status
- else
- :invalid
+ defp get_context(args) do
+ case args do
+ %{:context => context} -> context
+ _ -> "default"
end
end
- # Get the last user/context status from the database
- defp get_user_status(socket) do
- require Amnesia
- require Amnesia.Helper
-
- key = Integer.to_string(socket.assigns.user.id) <> ":" <> socket.assigns.context
-
- Amnesia.transaction do
- case Database.Status.read(key) do
- # use last status
- %Database.Status{status: status} -> status
- # otherwise set status to online
- _ -> :online
- end
- end
- end
-
- # Save the current user/context status to the database
- defp set_user_status(socket, :invalid), do: socket
- defp set_user_status(socket, status) do
- require Amnesia
- require Amnesia.Helper
-
- key = Integer.to_string(socket.assigns.user.id) <> ":" <> socket.assigns.context
-
- Amnesia.transaction do
- Database.Status.write(%Database.Status{key: key, status: status})
- end
-
- socket
- end
end
diff --git a/lib/kolab_chat/web/controllers/chat_controller.ex b/lib/kolab_chat/web/controllers/chat_controller.ex
index 8c3a972..053b4b4 100644
--- a/lib/kolab_chat/web/controllers/chat_controller.ex
+++ b/lib/kolab_chat/web/controllers/chat_controller.ex
@@ -1,27 +1,37 @@
defmodule KolabChat.Web.ChatController do
use KolabChat.Web, :controller
+ alias KolabChat.Rooms
+
plug :put_layout, "chat.html"
def index(conn, %{"room" => room} = params) do
+ metadata = Rooms.find(%{:id => room, :name => room})
create_room(conn, params, room)
end
def index(conn, params) do
create_room(conn, params, UUID.uuid4())
end
- defp create_room(conn, params, roomId) do
+ defp create_room(conn, params, roomId, room \\ %{}) do
+ room = %{
+ :id => roomId,
+ :creator => conn.assigns.user.id
+ }
+
+ Rooms.set(roomId, room)
+
conn
- |> assign(:room, roomId)
+ |> assign(:room, room)
|> assign_invitees(params)
|> render("index.html")
end
defp assign_invitees(conn, %{"invite" => invitees}) do
conn
|> assign(:invitees, invitees)
end
defp assign_invitees(conn, _params), do: conn
end
diff --git a/lib/kolab_chat/web/templates/chat/index.html.eex b/lib/kolab_chat/web/templates/chat/index.html.eex
index f792d39..48c51d8 100644
--- a/lib/kolab_chat/web/templates/chat/index.html.eex
+++ b/lib/kolab_chat/web/templates/chat/index.html.eex
@@ -1,3 +1,3 @@
-Room id is: <%= @room %>
+
diff --git a/lib/kolab_chat/web/templates/layout/chat.html.eex b/lib/kolab_chat/web/templates/layout/chat.html.eex
index dfe6eff..7dfacdb 100644
--- a/lib/kolab_chat/web/templates/layout/chat.html.eex
+++ b/lib/kolab_chat/web/templates/layout/chat.html.eex
@@ -1,34 +1,34 @@
<%= gettext "Kolab Real Time Communication" %>
">