diff --git a/assets/js/api.js b/assets/js/api.js
--- a/assets/js/api.js
+++ b/assets/js/api.js
@@ -2,8 +2,10 @@
import {Socket, LongPoll, Presence} from "phoenix"
import UserListWidget from "./widgets/userlist"
import UserStatusWidget from "./widgets/userstatus"
+import ChannelListWidget from "./widgets/channellist"
import ChatInputWidget from "./widgets/chatinput"
import ChatRoomWidget from "./widgets/chatroom"
+import ChatDetailsWidget from "./widgets/chatdetails"
class KolabChat
{
@@ -16,6 +18,8 @@
* - 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
+ * - chatDetailsElement: Id of HTML element where to put chat room details widget
+ * - channelListElement: Id of HTML element where to put channellist widget
*/
constructor(config)
{
@@ -64,6 +68,7 @@
if (this.config.userListElement && $('#' + this.config.userListElement).length) {
config = {
username: this.username,
+ title: true,
openChat: (e, user) => { this.openChat(e, user) }
}
this.userListWidget = new UserListWidget(this.config.userListElement, config)
@@ -77,6 +82,23 @@
this.userStatusWidget = new UserStatusWidget(this.config.userStatusElement, config)
}
+ if (this.config.channelListElement && $('#' + this.config.channelListElement).length) {
+ config = {
+ username: this.username,
+ title: true,
+ openChat: (e, roomId) => { this.openExistingChat(roomId) },
+ createChat: (e) => { this.openChat(e); }
+ }
+ this.channelListWidget = new ChannelListWidget(this.config.channelListElement, config)
+ }
+
+ if (this.config.chatDetailsElement && $('#' + this.config.chatDetailsElement).length) {
+ config = {
+ submit: (e, data) => { this.updateChatDetails(data); }
+ }
+ this.chatDetailsWidget = new ChatDetailsWidget(this.config.chatDetailsElement, config)
+ }
+
if (this.config.chatRoomElement && $('#' + this.config.chatRoomElement).length) {
this.chatRoomWidget = new ChatRoomWidget(this.config.chatRoomElement)
}
@@ -131,7 +153,8 @@
{
let channelName = roomId.startsWith("room:") ? roomId
: "room:" + roomId
- this.chat = this.socket.channel(channelName)
+
+ this.chat = this.socket.channel(channelName, {context: this.config.context})
if (this.chatRoomWidget) {
this.chat.on("new:message", message => {
@@ -139,6 +162,12 @@
})
}
+ if (this.chatDetailsWidget) {
+ this.chat.on("info", message => {
+ this.chatDetailsWidget.setRoom(message)
+ })
+ }
+
let join = this.chat.join()
if (invitees) {
@@ -151,7 +180,7 @@
*/
initNotifications(userId)
{
- this.notifications = this.socket.channel("user:" + userId)
+ this.notifications = this.socket.channel("user:" + userId, {context: this.config.context})
this.notifications.on("notify:invite", message => {
console.log("Invite from " + message.user + " to room " + message.room)
@@ -231,7 +260,11 @@
openChat(event, user)
{
let windowName = 'KolabChat' + new Date().getTime()
- let url = "/chat/?token=" + encodeURIComponent(this.config.token) + "&invite=" + encodeURIComponent(user)
+ let url = "/chat/?token=" + encodeURIComponent(this.config.token)
+
+ if (user) {
+ url += "&invite=" + encodeURIComponent(user)
+ }
var extwin = window.open(url, windowName);
}
@@ -248,6 +281,14 @@
var extwin = window.open(url, windowName);
}
+ /**
+ * Update chat room details (e.g. name)
+ */
+ updateChatDetails(data)
+ {
+ // TODO
+ }
+
static extend(obj, src)
{
Object.keys(src).forEach(function(key) { obj[key] = src[key] })
diff --git a/assets/js/app.js b/assets/js/app.js
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -24,7 +24,9 @@
let chat = new KolabChat({
userListElement: "userlist",
userStatusElement: "userstatus",
+ channelListElement: "channellist",
chatRoomElement: "chat_txt",
+ chatDetailsElement: "chat_details",
chatInputElement: "chat_txt_input"
})
diff --git a/assets/js/widgets/channellist.js b/assets/js/widgets/channellist.js
new file mode 100644
--- /dev/null
+++ b/assets/js/widgets/channellist.js
@@ -0,0 +1,87 @@
+
+class ChannelListWidget
+{
+ /**
+ * Configuration:
+ * - username: Current user name
+ * - openChat: Callback for "Open" button
+ * - createChat: Callback for "Create" button
+ * - title: Enable list header with Create button
+ */
+ constructor(id, config)
+ {
+ this.config = config || {}
+ this.id = id
+ this.render([])
+ }
+
+ setUser(username, userId)
+ {
+ this.config.username = username
+ this.config.userId = userId
+ }
+
+
+ /**
+ * Render channels list
+ */
+ render(channels)
+ {
+ let title = ''
+ let btn = ''
+ let list = $('#' + this.id)
+ let config = this.config
+ let html = channels.map(channel => {
+ // TODO: List only public channels and channels the user has been invited to
+ let buttons = this.buttons(channel)
+ return `
+
+ #${channel.name}
+ ${buttons}
+ `
+ })
+ .join("")
+
+ if (config.title === true) {
+ if (config.createChat) {
+ btn = `
+
+ `
+ }
+
+ title = `Channels ${btn}
` // TODO: Localization
+ }
+
+ list.html(title + '')
+
+ $('button', list).on('click', function(e) {
+ let action = $(this).data('action')
+ if (action && config[action]) {
+ config[action](e, $(this).parents('li').data('channel'))
+ }
+ })
+ }
+
+ /**
+ * Render channel list record buttons
+ */
+ buttons(channel)
+ {
+ let buttons = ''
+
+ if (this.config.openChat) {
+ let btn_name = ' Open' // TODO: localization
+ buttons += ``
+ }
+
+ if (buttons) {
+ buttons = '' + buttons + '
'
+ }
+
+ return buttons
+ }
+}
+
+export default ChannelListWidget
diff --git a/assets/js/widgets/chatdetails.js b/assets/js/widgets/chatdetails.js
new file mode 100644
--- /dev/null
+++ b/assets/js/widgets/chatdetails.js
@@ -0,0 +1,51 @@
+
+class ChatDetailsWidget
+{
+ /**
+ * Configuration:
+ * - submit: Callback for chat details update (only name for now)
+ */
+ constructor(id, config)
+ {
+ this.config = config || {}
+ this.id = id
+ this.render()
+ }
+
+ /**
+ * Set chat room details and re-render the widget
+ */
+ setRoom(room)
+ {
+ this.render(room)
+ }
+
+ /**
+ * Renders text chat input widget
+ */
+ render(room)
+ {
+ if (!room || !room.id) {
+ return $('#' + this.id).html('')
+ }
+
+ // TODO: Localization
+
+ let room_name = room.name || '';
+ let html = `
+ Room id: ${room.id}
+ Room name:
+ `
+
+ $('#' + 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)
+ }
+ })
+ }
+}
+
+export default ChatDetailsWidget
diff --git a/assets/js/widgets/userlist.js b/assets/js/widgets/userlist.js
--- a/assets/js/widgets/userlist.js
+++ b/assets/js/widgets/userlist.js
@@ -4,12 +4,14 @@
/**
* Configuration:
* - username: Current user name
- * - openChat: callback for "Open chat" button
+ * - openChat: Callback for "Open chat" button
+ * - title: Enables list header
*/
constructor(id, config)
{
this.config = config || {}
this.id = id
+ this.render([])
}
setUser(username, userId)
@@ -24,6 +26,7 @@
*/
render(presences)
{
+ let title = ''
let list = $('#' + this.id)
let config = this.config
let html = presences.map(presence => {
@@ -38,7 +41,11 @@
})
.join("")
- list.html(html)
+ if (config.title === true) {
+ title = 'Users
' // TODO: Localization
+ }
+
+ list.html(title + '')
$('button', list).on('click', function(e) {
let action = $(this).data('action')
@@ -56,7 +63,7 @@
let buttons = ''
if (this.config.openChat) {
- let btn_name = ' Open chat'
+ let btn_name = ' Open chat' // TODO: Localization
buttons += ``
}
diff --git a/assets/js/widgets/userstatus.js b/assets/js/widgets/userstatus.js
--- a/assets/js/widgets/userstatus.js
+++ b/assets/js/widgets/userstatus.js
@@ -22,6 +22,8 @@
"invisible",
"offline"
];
+
+ // TODO: render() with defalt state here
}
/**
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.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]),
]
diff --git a/lib/kolab_chat/rooms.ex b/lib/kolab_chat/rooms.ex
new file mode 100644
--- /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
--- a/lib/kolab_chat/web/channels/presence.ex
+++ b/lib/kolab_chat/web/channels/presence.ex
@@ -74,4 +74,83 @@
"""
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
--- a/lib/kolab_chat/web/channels/room_channel.ex
+++ b/lib/kolab_chat/web/channels/room_channel.ex
@@ -2,9 +2,16 @@
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
@@ -27,4 +34,19 @@
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
--- a/lib/kolab_chat/web/channels/system_channel.ex
+++ b/lib/kolab_chat/web/channels/system_channel.ex
@@ -1,25 +1,9 @@
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()}
@@ -27,21 +11,16 @@
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
@@ -53,58 +32,11 @@
{: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
--- a/lib/kolab_chat/web/controllers/chat_controller.ex
+++ b/lib/kolab_chat/web/controllers/chat_controller.ex
@@ -1,9 +1,12 @@
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
@@ -11,9 +14,16 @@
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
diff --git a/lib/kolab_chat/web/templates/chat/index.html.eex b/lib/kolab_chat/web/templates/chat/index.html.eex
--- 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
--- a/lib/kolab_chat/web/templates/layout/chat.html.eex
+++ b/lib/kolab_chat/web/templates/layout/chat.html.eex
@@ -19,7 +19,7 @@
<%= if @conn.assigns[:user] do %>
-
+