Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117764251
D443.1775222743.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
20 KB
Referenced Files
None
Subscribers
None
D443.1775222743.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)
}
@@ -139,6 +161,12 @@
})
}
+ if (this.chatDetailsWidget) {
+ this.chat.on("info", message => {
+ this.chatDetailsWidget.setRoom(message)
+ })
+ }
+
let join = this.chat.join()
if (invitees) {
@@ -231,7 +259,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 +280,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/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,13 @@
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
+ room = %{:id => room_name, :name => room_name} |> Rooms.find()
+ socket = assign(socket, :room, room)
+ send self(), :after_join
{:ok, socket}
end
@@ -27,4 +31,11 @@
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
+ {:noreply, 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, 1:25 PM (21 m, 25 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18822983
Default Alt Text
D443.1775222743.diff (20 KB)
Attached To
Mode
D443: More on channels management
Attached
Detach File
Event Timeline