Browse Source

[add][Closes #25] upload files

B Thibault 8 years ago
parent
commit
dfe4abb81c
10 changed files with 181 additions and 24 deletions
  1. 1 1
      cli/data.js
  2. 10 0
      cli/resources.js
  3. 38 0
      cli/ui.js
  4. 28 7
      cli/workflow.js
  5. 8 1
      srv/public/index.html
  6. 13 9
      srv/public/slack.min.js
  7. 9 0
      srv/public/style.css
  8. 25 1
      srv/src/httpServ.js
  9. 41 1
      srv/src/slack.js
  10. 8 4
      srv/src/slackData.js

+ 1 - 1
cli/data.js

@@ -37,7 +37,7 @@ SlackWrapper.prototype.update = function(data) {
         for (var i in data["live"]) {
             var history = this.history[i];
             if (!history)
-                this.history[i] = new SlackHistory(i, 500, data["live"][i]);
+                this.history[i] = new SlackHistory(i, 250, data["live"][i]);
             else
                 history.pushAll(data["live"][i]);
         }

+ 10 - 0
cli/resources.js

@@ -10,10 +10,20 @@ var R = {
             form: "msgForm"
             ,input: "msgInput"
             ,replyTo: "replyToContainer"
+            ,file: {
+                bt: "attachFile"
+                ,formContainer: "fileUploadContainer"
+                ,fileInput: "fileUploadInput"
+                ,form: "fileUploadForm"
+                ,error: "fileUploadError"
+                ,cancel: "fileUploadCancel"
+            }
+
         }
     }
     ,klass: {
         selected: "selected"
+        ,hidden: "hidden"
         ,noRoomSelected: "no-room-selected"
         ,noNetwork: "no-network"
         ,unread: "unread"

+ 38 - 0
cli/ui.js

@@ -83,6 +83,7 @@ function onRoomSelected() {
     onRoomUpdated();
     focusInput();
 
+    document.getElementById(R.id.message.file.formContainer).classList.add(R.klass.hidden);
     markRoomAsRead(SELECTED_ROOM);
     if (REPLYING_TO) {
         REPLYING_TO = null;
@@ -170,6 +171,8 @@ function createMessageDom(channelId, msg) {
     ;
     text.innerHTML = msgContent;
     authorName.textContent = sender ? sender.name : (msg.raw["username"] || "?");
+    if (!sender && !msg.raw["username"])
+        text.textContent = msg.raw["subtype"] || JSON.stringify(msg.raw);
     authorImg.src = sender ? sender.icons.image_48 : "";
     author.appendChild(authorImg);
     author.appendChild(authorName);
@@ -292,6 +295,41 @@ function focusInput() {
 document.addEventListener('DOMContentLoaded', function() {
     document.getElementById(R.id.chatList).addEventListener("click", onChanClick);
     document.getElementById(R.id.currentRoom.content).addEventListener("click", chatClickDelegate);
+    document.getElementById(R.id.message.file.cancel).addEventListener("click", function(e) {
+        e.preventDefault();
+        document.getElementById(R.id.message.file.error).classList.add(R.klass.hidden);
+        document.getElementById(R.id.message.file.formContainer).classList.add(R.klass.hidden);
+        document.getElementById(R.id.message.file.fileInput).value = "";
+        return false;
+    });
+    document.getElementById(R.id.message.file.form).addEventListener("submit", function(e) {
+        e.preventDefault();
+        var fileInput = document.getElementById(R.id.message.file.fileInput)
+            ,filename = fileInput.value;
+
+        if (filename) {
+            filename = filename.substr(filename.lastIndexOf('\\') +1);
+            uploadFile(SELECTED_ROOM, filename, fileInput.files[0], function(errorMsg) {
+                var error = document.getElementById(R.id.message.file.error);
+                if (errorMsg) {
+                    error.textContent = errorMsg;
+                    error.classList.remove(R.klass.hidden);
+                } else {
+                    error.classList.add(R.klass.hidden);
+                    document.getElementById(R.id.message.file.fileInput).value = "";
+                    document.getElementById(R.id.message.file.formContainer).classList.add(R.klass.hidden);
+                }
+            });
+        }
+        return false;
+    });
+    document.getElementById(R.id.message.file.bt).addEventListener("click", function(e) {
+        e.preventDefault();
+        if (SELECTED_ROOM) {
+            document.getElementById(R.id.message.file.formContainer).classList.remove(R.klass.hidden);
+        }
+        return false;
+    });
     document.getElementById(R.id.message.form).addEventListener("submit", function(e) {
         e.preventDefault();
         var input =document.getElementById(R.id.message.input);

+ 28 - 7
cli/workflow.js

@@ -93,19 +93,40 @@ function selectRoom(room) {
     document.body.classList.remove(R.klass.noRoomSelected);
     SELECTED_ROOM = room;
     onRoomSelected();
-    if (SELECTED_ROOM.lastRead && !SLACK.history[SELECTED_ROOM.id]) {
-        fetchHistory(SELECTED_ROOM, function(success) {
-            if (!success) {
-                // TODO handle error
-            }
-        });
-    }
+    if (SELECTED_ROOM.lastRead && !SLACK.history[SELECTED_ROOM.id])
+        fetchHistory(SELECTED_ROOM, function(success) {});
 }
 
 function unselectRoom() {
     document.getElementById(SELECTED_ROOM.id).classList.remove(R.klass.selected);
 }
 
+/**
+ * @param {SlackGroup|SlackChan|SlackIms} chan
+ * @param {string} filename
+ * @param {File} file
+ * @param {function(string?)} callback
+**/
+function uploadFile(chan, filename, file, callback) {
+    var fileReader = new FileReader()
+        ,formData = new FormData()
+        ,xhr = new XMLHttpRequest();
+
+    formData.append("file", file);
+    formData.append("filename", filename);
+    xhr.onreadystatechange = function() {
+        if (xhr.readyState === 4) {
+            if (xhr.status === 204) {
+                callback(null);
+            } else {
+                callback(xhr.statusText);
+            }
+        }
+    }
+    xhr.open('POST', 'api/file?room=' +chan.id);
+    xhr.send(formData);
+}
+
 /**
  * @param {SlackChan|SlackGroup|SlackIms} chan
  * @param {string} msg

+ 8 - 1
srv/public/index.html

@@ -18,10 +18,17 @@
                     <div id="replyToContainer" class="replyto-container">
                     </div>
                     <input type="text" id="msgInput" />
-                    <input type="submit" />
+                    <a id="attachFile" href="#!"><img src="paperclip.svg" alt="Send file" class="attach-file-icon" /></a>
+                    <input type="submit" class="button" />
                 </form>
             </div>
         </div>
+        <div class="hidden file-upload-container" id="fileUploadContainer"><form id="fileUploadForm" enctype="multipart/form-data">
+            <input type="file" id="fileUploadInput" />
+            <div id="fileUploadError" class="file-upload-error hidden"></div>
+            <input type="submit" class="button"/>
+            <a id="fileUploadCancel" class="button"/>cancel</a>
+        </form></div>
         <div class="error" id="neterror">Cannot connect to chat !</div>
         <script src="slack.min.js"></script>
     </body>

+ 13 - 9
srv/public/slack.min.js

@@ -1,17 +1,21 @@
 function g(b){this.id=b.id;this.name=b.name}function h(b,a){this.id=b.id;this.name=b.name;this.b=parseFloat(b.last_read);this.a={};if(b.members)for(var c=0,e=b.members.length;c<e;c++){var f=m(a,b.members[c]);this.a[f.id]=f;f.f[this.id]=this}}function n(b,a){var c=[];this.id=a.id;this.a={};for(var e=0,f=a.members.length;e<f;e++){var d=m(b,a.members[e]);this.a[a.members[e]]=d;d.f[this.id]=this;c.push(d.name)}this.name=c.join(", ");this.b=parseFloat(a.last_read)}
-function q(b,a){this.id=a.id;this.c=b;this.b=parseFloat(a.last_read)}function r(b){this.id=b.id;this.name=b.name;this.status=b.status;this.b={w:b.profile.image_24,A:b.profile.image_32,i:b.profile.image_48,m:b.profile.image_72,u:b.profile.image_192,C:b.profile.image_512};this.f={};this.a=null}function u(b){this.id=b.id;this.name=b.name;this.b={B:b.icons.image_36,i:b.icons.image_48,m:b.icons.image_72};this.f={};this.a=null}
+function q(b,a){this.id=a.id;this.c=b;this.b=parseFloat(a.last_read)}function r(b){this.id=b.id;this.name=b.name;this.status=b.status;this.b={w:b.profile.image_24,A:b.profile.image_32,i:b.profile.image_48,m:b.profile.image_72,u:b.profile.image_192,C:b.profile.image_512};this.f={};this.a=null}function v(b){this.id=b.id;this.name=b.name;this.b={B:b.icons.image_36,i:b.icons.image_48,m:b.icons.image_72};this.f={};this.a=null}
 function w(){this.j=null;this.f={};this.b={};this.c={};this.a={};this.g=null;this.h={}}function m(b,a){return b.a[a]||b.h[a]||null}function x(b,a){return b.f[a]||b.c[a]||b.b[a]||null}"undefined"!==typeof module&&(module.l.o=w);function z(b){this.c=b.user;this.b=parseFloat(b.ts);this.a=b}function A(b,a,c){this.id="string"===typeof b?b:b.id;this.a=[];this.b=a;c&&B(this,c)}function B(b,a){var c=0;a.forEach(function(b){c=Math.max(this.push(b),c)}.bind(b))}A.prototype.push=function(b){for(var a=parseFloat(b.ts),c=0,e=this.a.length;c<e;c++)if(this.a[c].b===a)return a;for(this.a.push(new z(b));this.a.length>this.b;)this.a.shift();return a};"undefined"!==typeof module&&(module.l.s=A);var C=null;
 function D(){var b=document.createDocumentFragment(),a=E.a.g?Object.keys(E.a.g.f):[];a.sort(function(b,a){return b[0]!==a[0]?b[0]-a[0]:(E.a.f[b]||E.a.b[b]).name.localeCompare((E.a.f[a]||E.a.b[a]).name)});a.forEach(function(a){a=E.a.f[a]||E.a.b[a];var c=document.createElement("li");c.id=a.id;"D"===a.id[0]?c.className="slack-context-room slack-ims":"G"===a.id[0]?c.className="slack-context-room slack-group":"C"===a.id[0]&&(c.className="slack-context-room slack-channel");c.textContent=a.name;c&&b.appendChild(c)});
 a=E.a.a?Object.keys(E.a.a):[];a.sort(function(b,a){return E.a.a[b].name.localeCompare(E.a.a[a].name)});a.forEach(function(a){a=E.a.a[a].a;var c=document.createElement("li");c.id=a.id;c.className="slack-context-room";c.textContent=a.c.name;c&&b.appendChild(c)});document.getElementById("chanList").textContent="";document.getElementById("chanList").appendChild(b)}function F(b){b?document.body.classList.remove("no-network"):document.body.classList.add("no-network")}
 function G(){if(C){document.body.classList.add("replyingTo");var b=document.getElementById("replyToContainer"),a=document.createElement("a");a.addEventListener("click",function(){C=null;G()});a.className="replyto-close";a.textContent="x";b.textContent="";b.appendChild(a);b.appendChild(H("reply_"+I.id,C))}else document.body.classList.remove("replyingTo")}
-function H(b,a){var c=document.createElement("div"),e=document.createElement("div"),f=document.createElement("div"),d=document.createElement("div"),k=document.createElement("img"),p=document.createElement("span"),l=document.createElement("ul"),t=document.createElement("li"),v=a.a.user?E.a.a[a.a.user]:E.a.h[a.a.bot_id];c.id=b+"_"+a.b;c.className="slackmsg-item";e.className="slackmsg-ts";f.className="slackmsg-msg";d.className="slackmsg-author";k.className="slackmsg-author-img";p.className="slackmsg-author-name";
+function H(b,a){var c=document.createElement("div"),e=document.createElement("div"),f=document.createElement("div"),d=document.createElement("div"),k=document.createElement("img"),p=document.createElement("span"),l=document.createElement("ul"),t=document.createElement("li"),u=a.a.user?E.a.a[a.a.user]:E.a.h[a.a.bot_id];c.id=b+"_"+a.b;c.className="slackmsg-item";e.className="slackmsg-ts";f.className="slackmsg-msg";d.className="slackmsg-author";k.className="slackmsg-author-img";p.className="slackmsg-author-name";
 l.className="slackmsg-hover";t.className="slackmsg-hover-reply";e.textContent=(new Date(1E3*a.b)).toLocaleTimeString();var y=a.a.text||"",y=y.replace(RegExp("<([@#]?)([^>]*)>","g"),function(a,b,c){a=c.split("|");"@"===b?(a[1]?"@"!==a[1][0]&&(a[1]="@"+a[1]):(c=m(E.a,a[0]),a[1]=c?"@"+c.name:"Unknown member"),a[0]="#"+a[0],a[2]="slackmsg-link slackmsg-link-user"):"#"===b?(a[1]?"#"!==a[1][0]&&(a[1]="#"+a[1]):(c=x(E.a,a[0]),a[1]=c?"#"+c.name:"Unknown channel"),a[0]="#"+a[0],a[2]="slackmsg-link slackmsg-link-chan"):
-(a[1]||(a[1]=a[0]),a[2]="slackmsg-link");return'<a href="'+a[0]+'" class="'+a[2]+'"'+(b?"":' target="_blank"')+">"+a[1]+"</a>"});f.innerHTML=y;p.textContent=v?v.name:a.a.username||"?";k.src=v?v.b.i:"";d.appendChild(k);d.appendChild(p);l.appendChild(t);c.appendChild(d);c.appendChild(f);c.appendChild(e);c.appendChild(l);return c}function J(){var b=0,a;for(a in K)K.hasOwnProperty(a)&&(b+=K[a]);document.title=(b?"("+b+") - ":"")+E.a.j.name}
-function L(){var b=I;K[b.id]&&(K[b.id]=0,J());b=document.getElementById(b.id);b.classList.remove("unread");b.classList.remove("unreadHi")}function M(){var b=document.createDocumentFragment(),a=I.id;document.getElementById("chatWindow").textContent="";E.b[a]&&E.b[a].a.forEach(function(c){b.appendChild(H(a,c))});var c=document.getElementById("chatWindow");c.appendChild(b);c.scrollTop=c.scrollHeight-c.clientHeight}
+(a[1]||(a[1]=a[0]),a[2]="slackmsg-link");return'<a href="'+a[0]+'" class="'+a[2]+'"'+(b?"":' target="_blank"')+">"+a[1]+"</a>"});f.innerHTML=y;p.textContent=u?u.name:a.a.username||"?";u||a.a.username||(f.textContent=a.a.subtype||JSON.stringify(a.a));k.src=u?u.b.i:"";d.appendChild(k);d.appendChild(p);l.appendChild(t);c.appendChild(d);c.appendChild(f);c.appendChild(e);c.appendChild(l);return c}
+function J(){var b=0,a;for(a in K)K.hasOwnProperty(a)&&(b+=K[a]);document.title=(b?"("+b+") - ":"")+E.a.j.name}function L(){var b=I;K[b.id]&&(K[b.id]=0,J());b=document.getElementById(b.id);b.classList.remove("unread");b.classList.remove("unreadHi")}
+function M(){var b=document.createDocumentFragment(),a=I.id;document.getElementById("chatWindow").textContent="";E.b[a]&&E.b[a].a.forEach(function(c){b.appendChild(H(a,c))});var c=document.getElementById("chatWindow");c.appendChild(b);c.scrollTop=c.scrollHeight-c.clientHeight}
 function N(b){for(;b.target!==b.currentTarget&&b.target;){if(b.target.classList.contains("slack-context-room")){if((b=E.a.f[b.target.id]||E.a.c[b.target.id]||E.a.b[b.target.id])&&b!==I){I&&document.getElementById(I.id).classList.remove("selected");document.getElementById(b.id).classList.add("selected");document.body.classList.remove("no-room-selected");I=b;b=void 0;var a=I.name||(I.c?I.c.name:void 0);if(!a){a=[];for(b in I.a)a.push(I.a[b].name);a=a.join(", ")}document.getElementById("currentRoomTitle").textContent=
-a;M();O();L();C&&(C=null,G());I.b&&!E.b[I.id]&&(b=new XMLHttpRequest,b.open("GET","api/hist?room="+I.id,!0),b.send(null))}break}b.target=b.target.parentElement}}
+a;M();O();document.getElementById("fileUploadContainer").classList.add("hidden");L();C&&(C=null,G());I.b&&!E.b[I.id]&&(b=new XMLHttpRequest,b.open("GET","api/hist?room="+I.id,!0),b.send(null))}break}b.target=b.target.parentElement}}
 function P(b){for(var a=b.target;a!==b.currentTarget&&a&&!a.classList.contains("slackmsg-hover");){if(a.classList.contains("slackmsg-hover-reply")){a:{for(a=a||b.target;a!==b.currentTarget&&a;){if(a.classList.contains("slackmsg-item")){b=a.id;break a}a=a.parentElement}b=void 0}if(b){b=parseFloat(b.split("_")[1]);for(var a=E.b[I.id].a,c=0,e=a.length;c<e&&a[c].b<=b;c++)if(a[c].b===b){C!==a[c]&&(C=a[c],G());break}}break}a=a.parentElement}}function O(){document.getElementById("msgInput").focus()}
-document.addEventListener("DOMContentLoaded",function(){document.getElementById("chatList").addEventListener("click",N);document.getElementById("chatWindow").addEventListener("click",P);document.getElementById("msgForm").addEventListener("submit",function(b){b.preventDefault();b=document.getElementById("msgInput");if(I&&b.value){var a=I,c=C,e=new XMLHttpRequest,f="api/msg?room="+a.id+"&text="+encodeURIComponent(b.value);if(c){var d=m(E.a,c.c),k="Message";"C"===a.id[0]?k="Channel message":"D"===a.id[0]?
-k="Direct message":"G"===a.id[0]&&(k="Group message");f+="&attachments="+encodeURIComponent(JSON.stringify([{fallback:c.a.text||"",author_name:"<@"+d.id+"|"+d.name+">",author_icon:d.b.i,text:c.a.text||"",footer:k,ts:c.b}]))}e.open("POST",f,!0);e.send(null);b.value="";C&&(C=null,G())}O();return!1});window.addEventListener("blur",function(){window.hasFocus=!1});window.addEventListener("focus",function(){window.hasFocus=!0;I&&L();O()});window.hasFocus=!0;Q()});var E,K={};E=new function(){this.c=0;this.a=new w;this.b={}};var R=0,I=null;function S(b){var a=new XMLHttpRequest;a.timeout=6E4;a.onreadystatechange=function(){if(4===a.readyState)if(a.status){var c=null,e=2===Math.floor(a.status/100);if(e){R&&(R=0,F(!0));c=a.response;try{c=JSON.parse(c)}catch(f){c=null}}else R?(R+=Math.floor((R||5)/2),R=Math.min(60,R)):(R=5,F(!1));b(e,c)}else R&&(R=0,F(!0)),S(b)};a.open("GET","api?v="+E.c,!0);a.send(null)}
-function T(b,a){if(b){if(a){var c=E;a.v&&(c.c=a.v);if(a["static"]){for(var e=c.a,f=a["static"],d=0,k=f.bots.length;d<k;d++)e.h[f.bots[d].id]=new u(f.bots[d]);d=0;for(k=f.users.length;d<k;d++)e.a[f.users[d].id]=new r(f.users[d]);d=0;for(k=f.ims.length;d<k;d++){var p=m(e,f.ims[d].user);p&&(p.a=new q(p,f.ims[d]),e.c[p.a.id]=p.a)}d=0;for(k=f.channels.length;d<k;d++)e.f[f.channels[d].id]=new h(f.channels[d],e);d=0;for(k=f.groups.length;d<k;d++)e.b[f.groups[d].id]=new n(e,f.groups[d]);e.j=new g(f.team);
-e.g=m(e,f.self.id);D()}if(a.live){for(var l in a.live)(e=c.b[l])?B(e,a.live[l]):c.b[l]=new A(l,500,a.live[l]);for(var t in a.live)l=x(c.a,t),e=a.live[t],!l||l===I&&window.hasFocus||(document.getElementById(l.id).classList.add("unread"),K[l.id]=(K[l.id]||0)+e.length,J()),I&&a.live[I.id]&&M()}}Q()}else setTimeout(Q,1E3*R)}function Q(){S(T)};
+document.addEventListener("DOMContentLoaded",function(){document.getElementById("chatList").addEventListener("click",N);document.getElementById("chatWindow").addEventListener("click",P);document.getElementById("fileUploadCancel").addEventListener("click",function(b){b.preventDefault();document.getElementById("fileUploadError").classList.add("hidden");document.getElementById("fileUploadContainer").classList.add("hidden");document.getElementById("fileUploadInput").value="";return!1});document.getElementById("fileUploadForm").addEventListener("submit",
+function(b){b.preventDefault();b=document.getElementById("fileUploadInput");var a=b.value;a&&(a=a.substr(a.lastIndexOf("\\")+1),Q(a,b.files[0],function(a){var b=document.getElementById("fileUploadError");a?(b.textContent=a,b.classList.remove("hidden")):(b.classList.add("hidden"),document.getElementById("fileUploadInput").value="",document.getElementById("fileUploadContainer").classList.add("hidden"))}));return!1});document.getElementById("attachFile").addEventListener("click",function(b){b.preventDefault();
+I&&document.getElementById("fileUploadContainer").classList.remove("hidden");return!1});document.getElementById("msgForm").addEventListener("submit",function(b){b.preventDefault();b=document.getElementById("msgInput");if(I&&b.value){var a=I,c=C,e=new XMLHttpRequest,f="api/msg?room="+a.id+"&text="+encodeURIComponent(b.value);if(c){var d=m(E.a,c.c),k="Message";"C"===a.id[0]?k="Channel message":"D"===a.id[0]?k="Direct message":"G"===a.id[0]&&(k="Group message");f+="&attachments="+encodeURIComponent(JSON.stringify([{fallback:c.a.text||
+"",author_name:"<@"+d.id+"|"+d.name+">",author_icon:d.b.i,text:c.a.text||"",footer:k,ts:c.b}]))}e.open("POST",f,!0);e.send(null);b.value="";C&&(C=null,G())}O();return!1});window.addEventListener("blur",function(){window.hasFocus=!1});window.addEventListener("focus",function(){window.hasFocus=!0;I&&L();O()});window.hasFocus=!0;R()});var E,K={};E=new function(){this.c=0;this.a=new w;this.b={}};var S=0,I=null;function T(b){var a=new XMLHttpRequest;a.timeout=6E4;a.onreadystatechange=function(){if(4===a.readyState)if(a.status){var c=null,e=2===Math.floor(a.status/100);if(e){S&&(S=0,F(!0));c=a.response;try{c=JSON.parse(c)}catch(f){c=null}}else S?(S+=Math.floor((S||5)/2),S=Math.min(60,S)):(S=5,F(!1));b(e,c)}else S&&(S=0,F(!0)),T(b)};a.open("GET","api?v="+E.c,!0);a.send(null)}
+function U(b,a){if(b){if(a){var c=E;a.v&&(c.c=a.v);if(a["static"]){for(var e=c.a,f=a["static"],d=0,k=f.bots.length;d<k;d++)e.h[f.bots[d].id]=new v(f.bots[d]);d=0;for(k=f.users.length;d<k;d++)e.a[f.users[d].id]=new r(f.users[d]);d=0;for(k=f.ims.length;d<k;d++){var p=m(e,f.ims[d].user);p&&(p.a=new q(p,f.ims[d]),e.c[p.a.id]=p.a)}d=0;for(k=f.channels.length;d<k;d++)e.f[f.channels[d].id]=new h(f.channels[d],e);d=0;for(k=f.groups.length;d<k;d++)e.b[f.groups[d].id]=new n(e,f.groups[d]);e.j=new g(f.team);
+e.g=m(e,f.self.id);D()}if(a.live){for(var l in a.live)(e=c.b[l])?B(e,a.live[l]):c.b[l]=new A(l,250,a.live[l]);for(var t in a.live)l=x(c.a,t),e=a.live[t],!l||l===I&&window.hasFocus||(document.getElementById(l.id).classList.add("unread"),K[l.id]=(K[l.id]||0)+e.length,J()),I&&a.live[I.id]&&M()}}R()}else setTimeout(R,1E3*S)}function R(){T(U)}
+function Q(b,a,c){var e=I;new FileReader;var f=new FormData,d=new XMLHttpRequest;f.append("file",a);f.append("filename",b);d.onreadystatechange=function(){4===d.readyState&&(204===d.status?c(null):c(d.statusText))};d.open("POST","api/file?room="+e.id);d.send(f)};

+ 9 - 0
srv/public/style.css

@@ -29,7 +29,9 @@ body {
     background-color: lightgrey;
 }
 
+.hidden { display: none; }
 .error { display: none; position: fixed; top: 0; left: 0; right: 0; height: 2em; line-height: 2em; background: #ffa2a2; padding: 0.5em 1em; }
+.button { border: 1px solid black; background: white; cursor: pointer; font: 13.3333px Arial; margin: 0; padding: 1px 6px; }
 .no-network .error { display: inline-block; }
 .slack-chat-content { height: calc(100vh - 8em); overflow: auto; }
 .slack-chat-title { display: inline-block; height: 1.75em; font-size: 1.75em; font-style: italic; }
@@ -56,3 +58,10 @@ body {
 .replyingTo .slack-chat-content { height: calc(100vh - 13em); }
 .replyto-container .replyto-close { position: absolute; display: inline-block; top: 15px; right: 0; height: 1.5em; width: 1.5em; text-align: center; cursor: pointer; z-index: 500; }
 
+.attach-file-icon { height: 1.5em; vertical-align: bottom; }
+
+.file-upload-container { position: fixed; z-index: 5000; background: rgba(55, 55, 55, 0.8); top: 0; bottom: 0; right: 0; left: 0; max-width: 100%; margin: auto; height: 200px; width: 500px; max-height: 100%; padding: 1.5em; border: 1px solid rgba(55, 55, 55, 1); border-radius: 10px; }
+.file-upload-container > form { display: inline-block; background: white; height: 100%; width: 100%; }
+.file-upload-container input { display: block; }
+.file-upload-error { margin: 1em 0; padding: 0.5em 1.5em; background: #ffa2a2; }
+

+ 25 - 1
srv/src/httpServ.js

@@ -148,8 +148,32 @@ Server.prototype.onRequest = function(req, res) {
             }
             sessionManager.saveSession(req.session);
             res.end();
+        } else if (req.urlObj.match(["api", "file"])) {
+            sessionManager.saveSession(req.session);
+            if (req.urlObj.queryTokens["room"]) {
+                var chan = res.slack.data.getChannel(req.urlObj.queryTokens["room"][0]);
+                if (chan) {
+                    var uploadRequest = res.slack.openUploadFileStream(chan, req.headers["content-type"], (errorMsg) => {
+                        if (!errorMsg)
+                            res.writeHeader("204", "No Content");
+                        else
+                            res.writeHeader("500", errorMsg);
+                        res.end();
+                    });
+                    req.on('end', () => {
+                        uploadRequest.end();
+                    });
+                    req.pipe(uploadRequest);
+                } else {
+                    res.writeHeader("404", "Channel Not Found");
+                    res.end();
+                }
+            } else {
+                res.writeHeader("400", "Bad Request");
+                res.end();
+            }
         } else if (req.urlObj.match(["api"])) {
-            res.slack.onRequest(req.urlObj.queryTokens.v || 0, (slack, newData) => {
+            res.slack.onRequest((req.urlObj.queryTokens.v ? req.urlObj.queryTokens.v[0] : 0) || 0, (slack, newData) => {
                 if (!res.ended) {
                     try {
                         if (!slack.connected) {

+ 41 - 1
srv/src/slack.js

@@ -10,6 +10,8 @@ const
 ;
 
 const SLACK_ENDPOINT = "https://slack.com/api/"
+    ,SLACK_HOSTNAME = "slack.com"
+    ,SLACK_ENDPOINT_PATH = "/api/"
     ,GETAPI = {
         rtmStart: "rtm.start"
         ,oauth: "oauth.access"
@@ -17,8 +19,9 @@ const SLACK_ENDPOINT = "https://slack.com/api/"
         ,directHistory: "im.history"
         ,groupHistory: "groups.history"
         ,postMsg: "chat.postMessage"
+        ,postFile: "files.upload"
     }
-    ,HISTORY_LENGTH = 25
+    ,HISTORY_LENGTH = 35
 ;
 
 function Slack(sess) {
@@ -198,6 +201,43 @@ Slack.getOauthToken = function(code, cb) {
     });
 };
 
+/**
+ * @param {SlackChan|SlackGroup|SlackIms} channel
+ * @param {string} contentType
+ * @param {function(string|null)} callback
+**/
+Slack.prototype.openUploadFileStream = function(channel, contentType, callback) {
+    var req = https.request({
+        hostname: SLACK_HOSTNAME
+        ,method: 'POST'
+        ,path: SLACK_ENDPOINT_PATH +GETAPI.postFile
+            +"?token=" +this.token
+            +"&channels=" +channel.id
+        ,headers: {
+            "Content-Type": contentType
+        }
+    }, (res) => {
+        var errorJson;
+        res.on("data", (chunk) => {
+            errorJson = errorJson ? Buffer.concat([errorJson, chunk], errorJson.length +chunk.length) : Buffer.from(chunk);
+        });
+        res.once("end", () => {
+            if (res.statusCode === 200) {
+                callback(null);
+            } else {
+                try {
+                    errorJson = JSON.parse(errorJson.toString());
+                } catch(e) {
+                    callback("error");
+                    return;
+                }
+                callback(errorJson["error"] || "error");
+            }
+        });
+    });
+    return req;
+};
+
 /**
  * @param {SlackChan|SlackGroup|SlackIms} channel
  * @param {Array.<string>} text

+ 8 - 4
srv/src/slackData.js

@@ -371,10 +371,12 @@ SlackBot.prototype.toStatic = function() {
 **/
 SlackData.prototype.updateStatic = function(data) {
     //TODO make lazy to keep pointers
-    for (var i =0, nbBots = data["bots"].length; i < nbBots; i++)
+    for (var i =0, nbBots = data["bots"].length; i < nbBots; i++) {
         this.bots[data["bots"][i].id] = new SlackBot(data["bots"][i]);
-    for (var i =0, nbUsers = data["users"].length; i < nbUsers; i++)
+    }
+    for (var i =0, nbUsers = data["users"].length; i < nbUsers; i++) {
         this.users[data["users"][i].id] = new SlackUser(data["users"][i]);
+    }
     for (var i =0, nbIms = data["ims"].length; i < nbIms; i++) {
         var user = this.getMember(data["ims"][i]["user"]);
         if (user) {
@@ -382,10 +384,12 @@ SlackData.prototype.updateStatic = function(data) {
             this.ims[user.ims.id] = user.ims;
         }
     }
-    for (var i =0, nbChan = data["channels"].length; i < nbChan; i++)
+    for (var i =0, nbChan = data["channels"].length; i < nbChan; i++) {
         this.channels[data["channels"][i].id] = new SlackChan(data["channels"][i], this);
-    for (var i =0, nbGroups = data["groups"].length; i < nbGroups; i++)
+    }
+    for (var i =0, nbGroups = data["groups"].length; i < nbGroups; i++) {
         this.groups[data["groups"][i]["id"]] = new SlackGroup(this, data["groups"][i]);
+    }
     this.team = new SlackTeam(data["team"]);
     this.staticV = parseFloat(data["latest_event_ts"]);
     this.self = this.getMember(data["self"]["id"]);