')
$('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 85647be..ec950bd 100644
--- a/lib/kolab_chat.ex
+++ b/lib/kolab_chat.ex
@@ -1,23 +1,25 @@
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, []),
- worker(KolabChat.RoomInvites, [])
+ # Start other workers
+ worker(KolabChat.RoomInvites, []),
+ worker(KolabChat.Rooms, [])
]
# 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 bf0a829..7b04eca 100644
--- a/lib/kolab_chat/web/channels/room_channel.ex
+++ b/lib/kolab_chat/web/channels/room_channel.ex
@@ -1,50 +1,74 @@
defmodule KolabChat.Web.RoomChannel do
use KolabChat.Web, :channel
alias KolabChat.Web.Endpoint
alias KolabChat.RoomInvites
+ alias KolabChat.Rooms
@spec join(topic :: binary(), args :: map(), socket :: pid()) :: {:ok, socket :: pid()}
- def join("room:" <> roomId, _, socket) do
- case RoomInvites.can_join?(socket.assigns.user.id, roomId) do
- true -> {:ok, socket}
+ def join("room:" <> room_name, args, socket) do
+ room = Rooms.find(%{:id => room_name, :name => room_name})
+
+ case not is_nil(room) and RoomInvites.can_join?(socket.assigns.user.id, room.id) do
+ true ->
+ socket = socket
+ |> assign(:room, room)
+ |> assign(:context, get_context(args))
+
+ send self(), :after_join
+ {:ok, socket}
_ -> {:error, :not_authorized}
end
end
def leave(socket, topic) do
IO.puts "#{topic} Has the following presences: #{inspect Presence.list(socket)}"
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
invite([invitee], socket)
{:noreply, socket}
end
def handle_in("ctl:invite", %{"users" => invitees}, socket) when is_list(invitees) do
invite(invitees, socket)
end
def terminate(_msg, socket) do
leave(socket, socket.topic)
:ok
end
defp invite(invitees, socket) do
"room:" <> roomId = socket.topic
RoomInvites.invite(invitees, roomId)
Enum.each(invitees, fn invitee ->
Endpoint.broadcast!("user:" <> invitee, "notify:invite",
%{user: socket.assigns.user.username, room: roomId})
end
)
{: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..c9587a0 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 4e44bf5..1f25593 100644
--- a/lib/kolab_chat/web/controllers/chat_controller.ex
+++ b/lib/kolab_chat/web/controllers/chat_controller.ex
@@ -1,59 +1,68 @@
defmodule KolabChat.Web.ChatController do
use KolabChat.Web, :controller
alias KolabChat.RoomInvites
+ 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 a room id for this new room; this will attempt to locate a pre-existing room
# if there is an appropriate one (e.g. a private room for 2 users)
roomId = generate_room_id(conn, params)
# since we are creating a new room here, automatically add the user to the invite list
RoomInvites.invite(conn.assigns.user.id, roomId)
create_room(conn, params, roomId)
end
defp create_room(conn, params, roomId) 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
defp generate_room_id(conn, %{"invite" => invitee}) when is_bitstring(invitee) do
select_best_room(conn, invitee)
end
defp generate_room_id(conn, %{"invite" => [invitee]}) do
select_best_room(conn, invitee)
end
defp generate_room_id(_conn, _params) do
generate_new_room_id()
end
defp select_best_room(conn, invitee) do
case KolabChat.RoomInvites.locate_private_room(conn.assigns.user.id, invitee) do
nil -> generate_new_room_id()
id -> id
end
end
defp generate_new_room_id() do
UUID.uuid4()
end
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" %>
">