Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117762273
D443.1775216164.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
D443.1775216164.diff
View Options
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 `
+ <li class="list-group-item" data-channel="${channel.id}">
+ <span class="glyphicon glyphicon-channel"></span> #${channel.name}
+ ${buttons}
+ </li>`
+ })
+ .join("")
+
+ if (config.title === true) {
+ if (config.createChat) {
+ btn = `
+ <button type="button" class="btn btn-primary btn-xs" data-action="createChat">
+ <span class="glyphicon glyphicon-plus-sign"></span> Create
+ </button>
+ `
+ }
+
+ title = `<div class="channellist-head">Channels ${btn}</div>` // TODO: Localization
+ }
+
+ list.html(title + '<ul class="channellist list-group">' + html + '</ul>')
+
+ $('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 = '<span class="glyphicon glyphicon-comment"></span> Open' // TODO: localization
+ 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 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 = `
+ <div>Room id: ${room.id}</div>
+ <div>Room name: <input name="chat_name" value="${room_name}" class="form-control" /></div>
+ `
+
+ $('#' + 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 = '<div class="userlist-head">Users</div>' // TODO: Localization
+ }
+
+ list.html(title + '<ul class="userlist list-group">' + html + '</ul>')
$('button', list).on('click', function(e) {
let action = $(this).data('action')
@@ -56,7 +63,7 @@
let buttons = ''
if (this.config.openChat) {
- let btn_name = '<span class="glyphicon glyphicon-comment"></span> Open chat'
+ let btn_name = '<span class="glyphicon glyphicon-comment"></span> Open chat' // TODO: Localization
buttons += `<button type="button" class="btn btn-primary btn-xs" data-action="openChat">${btn_name}</button>`
}
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 %>
+<div id="chat_details"></div>
<div id="chat_txt" class="textchat"></div>
<div id="chat_txt_input" class="textchatinput input-group"></div>
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 @@
</div> <!-- /container -->
<%= if @conn.assigns[:user] do %>
<script>window.userToken = "<%= Phoenix.Token.sign(@conn, "user", @conn.assigns[:user].id) %>"</script>
- <script>window.roomId = "<%= @room %>"</script>
+ <script>window.roomId = "<%= @room.id %>"</script>
<script>window.invitees =
<%= if @conn.assigns[:invitees] do %>
"<%= @invitees %>"
diff --git a/lib/kolab_chat/web/templates/page/index.html.eex b/lib/kolab_chat/web/templates/page/index.html.eex
--- a/lib/kolab_chat/web/templates/page/index.html.eex
+++ b/lib/kolab_chat/web/templates/page/index.html.eex
@@ -1,7 +1,7 @@
<div class="jumbotron">
<%= if @conn.assigns[:user] do %>
- Users List:
- <ul id="userlist" class="userlist list-group"></ul>
+ <div id="userlist"></div>
+ <div id="channellist"></div>
<% else %>
<h2><%= gettext "Welcome to %{name}", name: "Kolab Chat!" %></h2>
<p class="lead"><%= gettext "Real-time communication for the Kolab groupware system." %></p>
diff --git a/mix.lock b/mix.lock
--- a/mix.lock
+++ b/mix.lock
@@ -1,18 +1,18 @@
-%{"amnesia": {:hex, :amnesia, "0.2.7", "ffc2221bf72da4cfafbbb497adf9cf7e52138f1333cec5836187a53f94ae0665", [:mix], [{:exquisite, "~> 0.1.7", [hex: :exquisite, repo: "hexpm", optional: false]}], "hexpm"},
- "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
- "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
- "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [], [], "hexpm"},
- "credo": {:hex, :credo, "0.7.4", "0c33bcce4d574ce6df163cbc7d1ecb22de65713184355bd3be81cc4ab0ecaafa", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"},
- "exquisite": {:hex, :exquisite, "0.1.8", "ee8f56aae477287ce5e7dfcbc163a420cccbb73e680a6d80a09203e9ef514fa4", [], [], "hexpm"},
- "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [], [], "hexpm"},
- "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [], [], "hexpm"},
- "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], [], "hexpm"},
- "phoenix": {:hex, :phoenix, "1.3.0-rc.2", "53104ada25ba85fe160268c0dc826fe038bc074293730b4522fb9aca28d8aa13", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
- "phoenix_html": {:hex, :phoenix_html, "2.9.3", "1b5a2122cbf743aa242f54dced8a4f1cc778b8bd304f4b4c0043a6250c58e258", [], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
- "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.8", "4333f9c74190f485a74866beff2f9304f069d53f047f5fbb0fb8d1ee4c495f73", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2-rc", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"},
- "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.1", "c10ddf6237007c804bf2b8f3c4d5b99009b42eca3a0dfac04ea2d8001186056a", [], [], "hexpm"},
- "plug": {:hex, :plug, "1.3.5", "7503bfcd7091df2a9761ef8cecea666d1f2cc454cbbaf0afa0b6e259203b7031", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
- "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
- "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"},
- "remix": {:hex, :remix, "0.0.2", "f06115659d8ede8d725fae1708920ef73353a1b39efe6a232d2a38b1f2902109", [:mix], [], "hexpm"},
- "uuid": {:hex, :uuid, "1.1.7", "007afd58273bc0bc7f849c3bdc763e2f8124e83b957e515368c498b641f7ab69", [:mix], [], "hexpm"}}
+%{"amnesia": {:hex, :amnesia, "0.2.7", "ffc2221bf72da4cfafbbb497adf9cf7e52138f1333cec5836187a53f94ae0665", [:mix], [{:exquisite, "~> 0.1.7", [hex: :exquisite, optional: false]}]},
+ "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], []},
+ "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, optional: false]}]},
+ "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []},
+ "credo": {:hex, :credo, "0.7.4", "0c33bcce4d574ce6df163cbc7d1ecb22de65713184355bd3be81cc4ab0ecaafa", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, optional: false]}]},
+ "exquisite": {:hex, :exquisite, "0.1.8", "ee8f56aae477287ce5e7dfcbc163a420cccbb73e680a6d80a09203e9ef514fa4", [:mix], []},
+ "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []},
+ "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], []},
+ "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], []},
+ "phoenix": {:hex, :phoenix, "1.3.0-rc.2", "53104ada25ba85fe160268c0dc826fe038bc074293730b4522fb9aca28d8aa13", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, optional: false]}]},
+ "phoenix_html": {:hex, :phoenix_html, "2.9.3", "1b5a2122cbf743aa242f54dced8a4f1cc778b8bd304f4b4c0043a6250c58e258", [:mix], [{:plug, "~> 1.0", [hex: :plug, optional: false]}]},
+ "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.8", "4333f9c74190f485a74866beff2f9304f069d53f047f5fbb0fb8d1ee4c495f73", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2-rc", [hex: :phoenix, optional: false]}]},
+ "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.1", "c10ddf6237007c804bf2b8f3c4d5b99009b42eca3a0dfac04ea2d8001186056a", [:mix], []},
+ "plug": {:hex, :plug, "1.3.5", "7503bfcd7091df2a9761ef8cecea666d1f2cc454cbbaf0afa0b6e259203b7031", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]},
+ "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []},
+ "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], []},
+ "remix": {:hex, :remix, "0.0.2", "f06115659d8ede8d725fae1708920ef73353a1b39efe6a232d2a38b1f2902109", [:mix], []},
+ "uuid": {:hex, :uuid, "1.1.7", "007afd58273bc0bc7f849c3bdc763e2f8124e83b957e515368c498b641f7ab69", [:mix], []}}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 3, 11:36 AM (4 m, 31 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18818730
Default Alt Text
D443.1775216164.diff (26 KB)
Attached To
Mode
D443: More on channels management
Attached
Detach File
Event Timeline