diff --git a/api/file.go b/api/file.go index 3ef50fbbd5..219cf61037 100644 --- a/api/file.go +++ b/api/file.go @@ -33,7 +33,7 @@ func InitFile(r *mux.Router) { sr := r.PathPrefix("/files").Subrouter() sr.Handle("/upload", ApiUserRequired(uploadFile)).Methods("POST") - sr.Handle("/get/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:([A-Za-z0-9]+/)?.+(\\.[A-Za-z0-9]{3,})?}", ApiAppHandler(getFile)).Methods("GET") + sr.Handle("/get/{channel_id:[A-Za-z0-9]+}/{user_id:[A-Za-z0-9]+}/{filename:([A-Za-z0-9]+/)?.+(\\.[A-Za-z0-9]{3,})?}", ApiAppHandler(getFile)).Methods("GET", "HEAD") sr.Handle("/get_public_link", ApiUserRequired(getPublicLink)).Methods("POST") } @@ -261,7 +261,10 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "max-age=2592000, public") w.Header().Set("Content-Length", strconv.Itoa(len(f))) - w.Write(f) + + if r.Method != "HEAD" { + w.Write(f) + } } func asyncGetFile(path string, fileData chan []byte) { diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx new file mode 100644 index 0000000000..3cd7918873 --- /dev/null +++ b/web/react/components/file_attachment.jsx @@ -0,0 +1,121 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var utils = require('../utils/utils.jsx'); + +module.exports = React.createClass({ + displayName: "FileAttachment", + canSetState: false, + propTypes: { + // a list of file pathes displayed by the parent FileAttachmentList + filenames: React.PropTypes.arrayOf(React.PropTypes.string).isRequired, + // the index of this attachment preview in the parent FileAttachmentList + index: React.PropTypes.number.isRequired, + // the identifier of the modal dialog used to preview files + modalId: React.PropTypes.string.isRequired, + // handler for when the thumbnail is clicked + handleImageClick: React.PropTypes.func + }, + getInitialState: function() { + return {fileSize: -1}; + }, + componentDidMount: function() { + this.canSetState = true; + + var filename = this.props.filenames[this.props.index]; + + if (filename) { + var fileInfo = utils.splitFileLocation(filename); + var type = utils.getFileType(fileInfo.ext); + + // This is a temporary patch to fix issue with old files using absolute paths + if (fileInfo.path.indexOf("/api/v1/files/get") != -1) { + fileInfo.path = fileInfo.path.split("/api/v1/files/get")[1]; + } + fileInfo.path = utils.getWindowLocationOrigin() + "/api/v1/files/get" + fileInfo.path; + + if (type === "image") { + var self = this; + $('').attr('src', fileInfo.path+'_thumb.jpg').load(function(path, name){ return function() { + $(this).remove(); + if (name in self.refs) { + var imgDiv = self.refs[name].getDOMNode(); + + $(imgDiv).removeClass('post__load'); + $(imgDiv).addClass('post__image'); + + var re1 = new RegExp(' ', 'g'); + var re2 = new RegExp('\\(', 'g'); + var re3 = new RegExp('\\)', 'g'); + var url = path.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29'); + $(imgDiv).css('background-image', 'url('+url+'_thumb.jpg)'); + } + }}(fileInfo.path, filename)); + } + } + }, + componentWillUnmount: function() { + // keep track of when this component is mounted so that we can asynchronously change state without worrying about whether or not we're mounted + this.canSetState = false; + }, + shouldComponentUpdate: function(nextProps, nextState) { + // the only time this object should update is when it receives an updated file size which we can usually handle without re-rendering + if (nextState.fileSize != this.state.fileSize) { + if (this.refs.fileSize) { + // update the UI element to display the file size without re-rendering the whole component + this.refs.fileSize.getDOMNode().innerHTML = utils.fileSizeToString(nextState.fileSize); + + return false; + } else { + // we can't find the element that should hold the file size so we must not have rendered yet + return true; + } + } else { + return true; + } + }, + render: function() { + var filenames = this.props.filenames; + var filename = filenames[this.props.index]; + + var fileInfo = utils.splitFileLocation(filename); + var type = utils.getFileType(fileInfo.ext); + + var thumbnail; + if (type === "image") { + thumbnail =
; + } else { + thumbnail =
; + } + + var fileSizeString = ""; + if (this.state.fileSize < 0) { + var self = this; + + // asynchronously request the size of the file so that we can display it next to the thumbnail + utils.getFileSize(utils.getFileUrl(filename), function(fileSize) { + if (self.canSetState) { + self.setState({fileSize: fileSize}); + } + }); + } else { + fileSizeString = utils.fileSizeToString(this.state.fileSize); + } + + return ( +
+ + {thumbnail} + +
+
{decodeURIComponent(utils.getFileName(filename))}
+
+ {fileInfo.ext.toUpperCase()} + {fileSizeString} +
+
+
+ ); + } +}); diff --git a/web/react/components/file_attachment_list.jsx b/web/react/components/file_attachment_list.jsx new file mode 100644 index 0000000000..b924429577 --- /dev/null +++ b/web/react/components/file_attachment_list.jsx @@ -0,0 +1,49 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var ViewImageModal = require('./view_image.jsx'); +var FileAttachment = require('./file_attachment.jsx'); +var Constants = require('../utils/constants.jsx'); + +module.exports = React.createClass({ + displayName: "FileAttachmentList", + propTypes: { + // a list of file pathes displayed by this + filenames: React.PropTypes.arrayOf(React.PropTypes.string).isRequired, + // the identifier of the modal dialog used to preview files + modalId: React.PropTypes.string.isRequired, + // the channel that this is part of + channelId: React.PropTypes.string, + // the user that owns the post that this is attached to + userId: React.PropTypes.string + }, + getInitialState: function() { + return {startImgId: 0}; + }, + render: function() { + var filenames = this.props.filenames; + var modalId = this.props.modalId; + + var postFiles = []; + for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) { + postFiles.push(); + } + + return ( +
+
+ {postFiles} +
+ +
+ ); + }, + handleImageClick: function(e) { + this.setState({startImgId: parseInt($(e.target.parentNode).attr('data-img-id'))}); + } +}); diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index 641ffeef27..860c96d842 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -1,63 +1,23 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. -var CreateComment = require( './create_comment.jsx' ); +var FileAttachmentList = require('./file_attachment_list.jsx'); var UserStore = require('../stores/user_store.jsx'); var utils = require('../utils/utils.jsx'); -var ViewImageModal = require('./view_image.jsx'); -var Constants = require('../utils/constants.jsx'); module.exports = React.createClass({ - handleImageClick: function(e) { - this.setState({startImgId: parseInt($(e.target.parentNode).attr('data-img-id'))}); - }, componentWillReceiveProps: function(nextProps) { var linkData = utils.extractLinks(nextProps.post.message); this.setState({ links: linkData["links"], message: linkData["text"] }); }, - componentDidMount: function() { - var filenames = this.props.post.filenames; - var self = this; - if (filenames) { - var re1 = new RegExp(' ', 'g'); - var re2 = new RegExp('\\(', 'g'); - var re3 = new RegExp('\\)', 'g'); - for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) { - var fileInfo = utils.splitFileLocation(filenames[i]); - if (Object.keys(fileInfo).length === 0) continue; - - var type = utils.getFileType(fileInfo.ext); - - // This is a temporary patch to fix issue with old files using absolute paths - if (fileInfo.path.indexOf("/api/v1/files/get") != -1) { - fileInfo.path = fileInfo.path.split("/api/v1/files/get")[1]; - } - fileInfo.path = utils.getWindowLocationOrigin() + "/api/v1/files/get" + fileInfo.path; - - if (type === "image") { - $('').attr('src', fileInfo.path+'_thumb.jpg').load(function(path, name){ return function() { - $(this).remove(); - if (name in self.refs) { - var imgDiv = self.refs[name].getDOMNode(); - $(imgDiv).removeClass('post__load'); - $(imgDiv).addClass('post__image'); - var url = path.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29'); - $(imgDiv).css('background-image', 'url('+url+'_thumb.jpg)'); - } - }}(fileInfo.path, filenames[i])); - } - } - } - }, getInitialState: function() { var linkData = utils.extractLinks(this.props.post.message); - return { startImgId: 0, links: linkData["links"], message: linkData["text"] }; + return { links: linkData["links"], message: linkData["text"] }; }, render: function() { var post = this.props.post; var filenames = this.props.post.filenames; var parentPost = this.props.parentPost; - var postImageModalId = "view_image_modal_" + post.id; var inner = utils.textToJsx(this.state.message); var comment = ""; @@ -99,44 +59,8 @@ module.exports = React.createClass({ postClass += " post-comment"; } - var postFiles = []; - var images = []; - if (filenames) { - for (var i = 0; i < filenames.length; i++) { - var fileInfo = utils.splitFileLocation(filenames[i]); - if (Object.keys(fileInfo).length === 0) continue; - - var type = utils.getFileType(fileInfo.ext); - - // This is a temporary patch to fix issue with old files using absolute paths - if (fileInfo.path.indexOf("/api/v1/files/get") != -1) { - fileInfo.path = fileInfo.path.split("/api/v1/files/get")[1]; - } - fileInfo.path = utils.getWindowLocationOrigin() + "/api/v1/files/get" + fileInfo.path; - - if (type === "image") { - if (i < Constants.MAX_DISPLAY_FILES) { - postFiles.push( -
-
-
- ); - } - images.push(filenames[i]); - } else if (i < Constants.MAX_DISPLAY_FILES) { - postFiles.push( -
- -
- -
- ); - } - } - } - var embed; - if (postFiles.length === 0 && this.state.links) { + if (filenames.length === 0 && this.state.links) { embed = utils.getEmbed(this.state.links[0]); } @@ -145,21 +69,13 @@ module.exports = React.createClass({ { comment }

{inner}

{ filenames && filenames.length > 0 ? -
- { postFiles } -
+ : "" } { embed } - - { images.length > 0 ? - - : "" }
); } diff --git a/web/react/components/post_right.jsx b/web/react/components/post_right.jsx index 8097a181e9..ad8b54012d 100644 --- a/web/react/components/post_right.jsx +++ b/web/react/components/post_right.jsx @@ -10,7 +10,7 @@ var utils = require('../utils/utils.jsx'); var SearchBox =require('./search_bar.jsx'); var CreateComment = require( './create_comment.jsx' ); var Constants = require('../utils/constants.jsx'); -var ViewImageModal = require('./view_image.jsx'); +var FileAttachmentList = require('./file_attachment_list.jsx'); var ActionTypes = Constants.ActionTypes; RhsHeaderPost = React.createClass({ @@ -55,28 +55,20 @@ RhsHeaderPost = React.createClass({ }); RootPost = React.createClass({ - handleImageClick: function(e) { - this.setState({startImgId: parseInt($(e.target.parentNode).attr('data-img-id'))}); - }, - getInitialState: function() { - return { startImgId: 0 }; - }, render: function() { - - var postImageModalId = "rhs_view_image_modal_" + this.props.post.id; - var message = utils.textToJsx(this.props.post.message); - var filenames = this.props.post.filenames; - var isOwner = UserStore.getCurrentId() == this.props.post.user_id; - var timestamp = UserStore.getProfile(this.props.post.user_id).update_at; - var channel = ChannelStore.get(this.props.post.channel_id); + var post = this.props.post; + var message = utils.textToJsx(post.message); + var isOwner = UserStore.getCurrentId() == post.user_id; + var timestamp = UserStore.getProfile(post.user_id).update_at; + var channel = ChannelStore.get(post.channel_id); var type = "Post"; - if (this.props.post.root_id.length > 0) { + if (post.root_id.length > 0) { type = "Comment"; } var currentUserCss = ""; - if (UserStore.getCurrentId() === this.props.post.user_id) { + if (UserStore.getCurrentId() === post.user_id) { currentUserCss = "current--user"; } @@ -84,60 +76,24 @@ RootPost = React.createClass({ channelName = (channel.type === 'D') ? "Private Message" : channel.display_name; } - if (filenames) { - var postFiles = []; - var images = []; - var re1 = new RegExp(' ', 'g'); - var re2 = new RegExp('\\(', 'g'); - var re3 = new RegExp('\\)', 'g'); - for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) { - var fileInfo = utils.splitFileLocation(filenames[i]); - var ftype = utils.getFileType(fileInfo.ext); - - // This is a temporary patch to fix issue with old files using absolute paths - if (fileInfo.path.indexOf("/api/v1/files/get") != -1) { - fileInfo.path = fileInfo.path.split("/api/v1/files/get")[1]; - } - fileInfo.path = utils.getWindowLocationOrigin() + "/api/v1/files/get" + fileInfo.path; - - if (ftype === "image") { - var url = fileInfo.path.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29'); - postFiles.push( -
-
-
- ); - images.push(filenames[i]); - } else { - postFiles.push( -
- -
- -
- ); - } - } - } - return (
{ channelName }
- +
    -
  • -
  • +
  • +
  • { isOwner ? : "" } @@ -146,19 +102,12 @@ RootPost = React.createClass({

{message}

- { filenames.length > 0 ? -
- { postFiles } -
- : "" } - { images.length > 0 ? - + { post.filenames && post.filenames.length > 0 ? + : "" }
@@ -169,86 +118,42 @@ RootPost = React.createClass({ }); CommentPost = React.createClass({ - handleImageClick: function(e) { - this.setState({startImgId: parseInt($(e.target.parentNode).attr('data-img-id'))}); - }, - getInitialState: function() { - return { startImgId: 0 }; - }, render: function() { + var post = this.props.post; var commentClass = "post"; var currentUserCss = ""; - if (UserStore.getCurrentId() === this.props.post.user_id) { + if (UserStore.getCurrentId() === post.user_id) { currentUserCss = "current--user"; } - var postImageModalId = "rhs_comment_view_image_modal_" + this.props.post.id; - var filenames = this.props.post.filenames; - var isOwner = UserStore.getCurrentId() == this.props.post.user_id; + var isOwner = UserStore.getCurrentId() == post.user_id; var type = "Post" - if (this.props.post.root_id.length > 0) { + if (post.root_id.length > 0) { type = "Comment" } - if (filenames) { - var postFiles = []; - var images = []; - var re1 = new RegExp(' ', 'g'); - var re2 = new RegExp('\\(', 'g'); - var re3 = new RegExp('\\)', 'g'); - for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) { - - var fileInfo = utils.splitFileLocation(filenames[i]); - var type = utils.getFileType(fileInfo.ext); - - // This is a temporary patch to fix issue with old files using absolute paths - if (fileInfo.path.indexOf("/api/v1/files/get") != -1) { - fileInfo.path = fileInfo.path.split("/api/v1/files/get")[1]; - } - fileInfo.path = utils.getWindowLocationOrigin() + "/api/v1/files/get" + fileInfo.path; - - if (type === "image") { - var url = fileInfo.path.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29'); - postFiles.push( -
-
-
- ); - images.push(filenames[i]); - } else { - postFiles.push( -
- -
- -
- ); - } - } - } - - var message = utils.textToJsx(this.props.post.message); + var message = utils.textToJsx(post.message); var timestamp = UserStore.getCurrentUser().update_at; return (
- +
    -
  • -
  • +
  • +
  • { isOwner ? : "" } @@ -256,19 +161,12 @@ CommentPost = React.createClass({

{message}

- { filenames.length > 0 ? -
- { postFiles } -
- : "" } - { images.length > 0 ? - + { post.filenames && post.filenames.length > 0 ? + : "" }
diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index 7b096c6298..4b2f8f6508 100644 --- a/web/react/components/view_image.jsx +++ b/web/react/components/view_image.jsx @@ -5,6 +5,8 @@ var Client = require('../utils/client.jsx'); var utils = require('../utils/utils.jsx'); module.exports = React.createClass({ + displayName: "ViewImageModal", + canSetState: false, handleNext: function() { var id = this.state.imgId + 1; if (id > this.props.filenames.length-1) { @@ -31,42 +33,41 @@ module.exports = React.createClass({ return; }; - var src = ""; - if (this.props.imgCount > 0) { - src = this.props.filenames[id]; - } else { - var fileInfo = utils.splitFileLocation(this.props.filenames[id]); - // This is a temporary patch to fix issue with old files using absolute paths - if (fileInfo.path.indexOf("/api/v1/files/get") !== -1) { - fileInfo.path = fileInfo.path.split("/api/v1/files/get")[1]; - } - fileInfo.path = utils.getWindowLocationOrigin() + "/api/v1/files/get" + fileInfo.path; - src = fileInfo['path'] + '_preview.jpg'; - } + var filename = this.props.filenames[id]; - var self = this; - var img = new Image(); - img.load(src, - function(){ - var progress = self.state.progress; - progress[id] = img.completedPercentage; - self.setState({ progress: progress }); - }); - img.onload = function(imgid) { - return function() { - var loaded = self.state.loaded; - loaded[imgid] = true; - self.setState({ loaded: loaded }); - $(self.refs.image.getDOMNode()).css("max-height",imgHeight); - }; - }(id); - var images = this.state.images; - images[id] = img; - this.setState({ images: images }); + var fileInfo = utils.splitFileLocation(filename); + var fileType = utils.getFileType(fileInfo.ext); + + if (fileType === "image") { + var self = this; + var img = new Image(); + img.load(this.getPreviewImagePath(filename), + function(){ + var progress = self.state.progress; + progress[id] = img.completedPercentage; + self.setState({ progress: progress }); + }); + img.onload = function(imgid) { + return function() { + var loaded = self.state.loaded; + loaded[imgid] = true; + self.setState({ loaded: loaded }); + $(self.refs.image.getDOMNode()).css("max-height",imgHeight); + }; + }(id); + var images = this.state.images; + images[id] = img; + this.setState({ images: images }); + } else { + // there's nothing to load for non-image files + var loaded = this.state.loaded; + loaded[id] = true; + this.setState({ loaded: loaded }); + } }, componentDidUpdate: function() { - if (this.refs.image) { - if (this.state.loaded[this.state.imgId]) { + if (this.state.loaded[this.state.imgId]) { + if (this.refs.imageWrap) { $(this.refs.imageWrap.getDOMNode()).removeClass("default"); } } @@ -91,6 +92,12 @@ module.exports = React.createClass({ $(self.refs.imageFooter.getDOMNode()).removeClass("footer--show"); } ); + + // keep track of whether or not this component is mounted so we can safely set the state asynchronously + this.canSetState = true; + }, + componentWillUnmount: function() { + this.canSetState = false; }, getPublicLink: function(e) { data = {}; @@ -112,62 +119,78 @@ module.exports = React.createClass({ loaded.push(false); progress.push(0); } - return { imgId: this.props.startId, viewed: false, loaded: loaded, progress: progress, images: {} }; + return { imgId: this.props.startId, viewed: false, loaded: loaded, progress: progress, images: {}, fileSizes: {} }; }, render: function() { if (this.props.filenames.length < 1 || this.props.filenames.length-1 < this.state.imgId) { return
; } - var fileInfo = utils.splitFileLocation(this.props.filenames[this.state.imgId]); + var filename = this.props.filenames[this.state.imgId]; + var fileUrl = utils.getFileUrl(filename); - var name = fileInfo['name'] + '.' + fileInfo['ext']; + var name = decodeURIComponent(utils.getFileName(filename)); - var loading = ""; + var content; var bgClass = ""; - var img = {}; - if (!this.state.loaded[this.state.imgId]) { + if (this.state.loaded[this.state.imgId]) { + var fileInfo = utils.splitFileLocation(filename); + var fileType = utils.getFileType(fileInfo.ext); + + if (fileType === "image") { + // image files just show a preview of the file + content = ( + + + + ); + } else { + // non-image files include a section providing details about the file + var infoString = "File type " + fileInfo.ext.toUpperCase(); + if (this.state.fileSizes[filename] && this.state.fileSizes[filename] >= 0) { + infoString += ", Size " + utils.fileSizeToString(this.state.fileSizes[filename]); + } + + content = ( +
+ + + + +
+
{name}
+
{infoString}
+
+
+ ); + + // asynchronously request the actual size of this file + if (!(filename in this.state.fileSizes)) { + var self = this; + + utils.getFileSize(utils.getFileUrl(filename), function(fileSize) { + if (self.canSetState) { + var fileSizes = self.state.fileSizes; + fileSizes[filename] = fileSize; + self.setState(fileSizes); + } + }); + } + } + } else { + // display a progress indicator when the preview for an image is still loading var percentage = Math.floor(this.state.progress[this.state.imgId]); - loading = ( -
- + content = ( +
+ { percentage > 0 ? {"Previewing " + percentage + "%"} : ""}
); bgClass = "black-bg"; - } else if (this.state.viewed) { - for (var id in this.state.images) { - var info = utils.splitFileLocation(this.props.filenames[id]); - var preview_filename = ""; - if (this.props.imgCount > 0) { - preview_filename = this.props.filenames[this.state.imgId]; - } else { - // This is a temporary patch to fix issue with old files using absolute paths - if (info.path.indexOf("/api/v1/files/get") !== -1) { - info.path = info.path.split("/api/v1/files/get")[1]; - } - info.path = utils.getWindowLocationOrigin() + "/api/v1/files/get" + info.path; - preview_filename = info['path'] + '_preview.jpg'; - } - - var imgClass = "hidden"; - if (this.state.loaded[id] && this.state.imgId == id) imgClass = ""; - - img[info['path']] = ; - } } - var imgFragment = React.addons.createFragment(img); - - // This is a temporary patch to fix issue with old files using absolute paths - var download_link = this.props.filenames[this.state.imgId]; - if (download_link.indexOf("/api/v1/files/get") !== -1) { - download_link = download_link.split("/api/v1/files/get")[1]; - } - download_link = utils.getWindowLocationOrigin() + "/api/v1/files/get" + download_link; - return ( ); + }, + // Returns the path to a preview image that can be used to represent a file. + getPreviewImagePath: function(filename) { + var fileInfo = utils.splitFileLocation(filename); + var fileType = utils.getFileType(fileInfo.ext); + + if (fileType === "image") { + // This is a temporary patch to fix issue with old files using absolute paths + if (fileInfo.path.indexOf("/api/v1/files/get") !== -1) { + fileInfo.path = fileInfo.path.split("/api/v1/files/get")[1]; + } + fileInfo.path = utils.getWindowLocationOrigin() + "/api/v1/files/get" + fileInfo.path; + + return fileInfo.path + '_preview.jpg'; + } else { + // only images have proper previews, so just use a placeholder icon for non-images + return utils.getPreviewImagePathForFileType(fileType); + } } }); diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 9a663a9360..e51f7f3f49 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -533,6 +533,19 @@ module.exports.getFileType = function(ext) { return "other"; }; +module.exports.getPreviewImagePathForFileType = function(fileType) { + fileType = fileType.toLowerCase(); + + var icon; + if (fileType in Constants.ICON_FROM_TYPE) { + icon = Constants.ICON_FROM_TYPE[fileType]; + } else { + icon = Constants.ICON_FROM_TYPE["other"]; + } + + return "/static/images/icons/" + icon + ".png"; +}; + module.exports.getIconClassName = function(fileType) { fileType = fileType.toLowerCase(); @@ -557,6 +570,23 @@ module.exports.splitFileLocation = function(fileLocation) { return {'ext': ext, 'name': filename, 'path': filePath}; } +// Asynchronously gets the size of a file by requesting its headers. If successful, it calls the +// provided callback with the file size in bytes as the argument. +module.exports.getFileSize = function(url, callback) { + var request = new XMLHttpRequest(); + + request.open('HEAD', url, true); + request.onreadystatechange = function() { + if (request.readyState == 4 && request.status == 200) { + if (callback) { + callback(parseInt(request.getResponseHeader("content-length"))); + } + } + }; + + request.send(); +}; + module.exports.toTitleCase = function(str) { return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();}); } @@ -847,4 +877,39 @@ module.exports.getWindowLocationOrigin = function() { windowLocationOrigin = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port: ''); } return windowLocationOrigin; +} + +// Converts a file size in bytes into a human-readable string of the form "123MB". +module.exports.fileSizeToString = function(bytes) { + // it's unlikely that we'll have files bigger than this + if (bytes > 1024 * 1024 * 1024 * 1024) { + return Math.floor(bytes / (1024 * 1024 * 1024 * 1024)) + "TB"; + } else if (bytes > 1024 * 1024 * 1024) { + return Math.floor(bytes / (1024 * 1024 * 1024)) + "GB"; + } else if (bytes > 1024 * 1024) { + return Math.floor(bytes / (1024 * 1024)) + "MB"; + } else if (bytes > 1024) { + return Math.floor(bytes / 1024) + "KB"; + } else { + return bytes + "B"; + } +}; + +// Converts a filename (like those attached to Post objects) to a url that can be used to retrieve attachments from the server. +module.exports.getFileUrl = function(filename) { + var url = filename; + + // This is a temporary patch to fix issue with old files using absolute paths + if (url.indexOf("/api/v1/files/get") != -1) { + url = filename.split("/api/v1/files/get")[1]; + } + url = module.exports.getWindowLocationOrigin() + "/api/v1/files/get" + url; + + return url; +}; + +// Gets the name of a file (including extension) from a given url or file path. +module.exports.getFileName = function(path) { + var split = path.split('/'); + return split[split.length - 1]; }; diff --git a/web/sass-files/sass/partials/_files.scss b/web/sass-files/sass/partials/_files.scss index 56d03e1719..ea75482679 100644 --- a/web/sass-files/sass/partials/_files.scss +++ b/web/sass-files/sass/partials/_files.scss @@ -14,10 +14,6 @@ position: relative; border: 1px solid #DDD; @include clearfix; - &.custom-file { - width: 85px; - height: 100px; - } &:hover .remove-preview:after { @include opacity(1); } @@ -71,60 +67,56 @@ width:300px; height:300px; } + +@mixin file-icon($path) { + background: #fff url($path); + background-position: center; + background-repeat: no-repeat; + @include background-size(60px auto); +} .file-icon { width: 100%; height: 100%; &.audio { - background: url("../images/icons/audio.png"); - @include background-size(100% 100%); + @include file-icon("../images/icons/audio.png"); } &.video { - background: url("../images/icons/video.png"); - @include background-size(100% 100%); + @include file-icon("../images/icons/video.png"); } &.ppt { - background: url("../images/icons/ppt.png"); - @include background-size(100% 100%); + @include file-icon("../images/icons/ppt.png"); } &.generic { - background: url("../images/icons/generic.png"); - @include background-size(100% 100%); + @include file-icon("../images/icons/generic.png"); } &.code { - background: url("../images/icons/code.png"); - @include background-size(100% 100%); + @include file-icon("../images/icons/code.png"); } &.excel { - background: url("../images/icons/excel.png"); - @include background-size(100% 100%); + @include file-icon("../images/icons/excel.png"); } &.word { - background: url("../images/icons/word.png"); - @include background-size(100% 100%); + @include file-icon("../images/icons/word.png"); } &.pdf { - background: url("../images/icons/pdf.png"); - @include background-size(100% 100%); + @include file-icon("../images/icons/pdf.png"); } &.patch { - background: url("../images/icons/patch.png"); - @include background-size(100% 100%); + @include file-icon("../images/icons/patch.png"); } &.image { - background: url("../images/icons/image.png"); - @include background-size(100% 100%); + @include file-icon("../images/icons/image.png"); } } .post-image__column { position: relative; - width: 120px; + width: 240px; height: 100px; float: left; margin: 5px 10px 5px 0; - &.custom-file { - width: 85px; - height: 100px; - } + @include display-flex; + display: -ms-flexbox; + border: 1px solid lightgrey; .post__load { height: 100%; width: 100%; @@ -133,15 +125,69 @@ background-position: center; } .post__image { - height: 100%; width: 100%; - border: 1px solid #E2E2E2; + height: 100%; background-color: #FFF; background-repeat: no-repeat; background-position: top left; } + .post-image__thumbnail { + width: 50%; + height: 100%; + } + .post-image__details { + width: 50%; + height: 100%; + background: white; + border-left: 1px solid #ddd; + font-size: 13px; + padding: 7px; + .post-image__name { + margin-bottom: 3px; + } + .post-image__type { + color: grey; + } + .post-image__size { + margin-left: 4px; + color: grey; + } + } a { text-decoration: none; color: grey; } } + +.file-details__container { + @include display-flex; + display: -ms-flexbox; + + .file-details { + width: 320px; + height: 270px; + padding: 14px; + text-align: left; + vertical-align: top; + + .file-details__name { + font-size: 16px; + } + .file-details__info { + color: grey; + } + } + .file-details__preview { + width: 320px; + height: 270px; + border-right: 1px solid #ddd; + vertical-align: center; + + // helper to center the image icon in the preview window + .file-details__preview-helper { + height: 100%; + display: inline-block; + vertical-align: middle; + } + } +} diff --git a/web/static/images/icons/audio.png b/web/static/images/icons/audio.png index 2b6d37f8d1..bd25b7f840 100644 Binary files a/web/static/images/icons/audio.png and b/web/static/images/icons/audio.png differ diff --git a/web/static/images/icons/code.png b/web/static/images/icons/code.png index 80db302ee3..c59e4b8dc0 100644 Binary files a/web/static/images/icons/code.png and b/web/static/images/icons/code.png differ diff --git a/web/static/images/icons/excel.png b/web/static/images/icons/excel.png index 70ddadcbf4..275c65c4d5 100644 Binary files a/web/static/images/icons/excel.png and b/web/static/images/icons/excel.png differ diff --git a/web/static/images/icons/generic.png b/web/static/images/icons/generic.png index d9e82c2329..0eb82c2d2d 100644 Binary files a/web/static/images/icons/generic.png and b/web/static/images/icons/generic.png differ diff --git a/web/static/images/icons/image.png b/web/static/images/icons/image.png index a3acdef4c2..7993177315 100644 Binary files a/web/static/images/icons/image.png and b/web/static/images/icons/image.png differ diff --git a/web/static/images/icons/patch.png b/web/static/images/icons/patch.png index 18af126d47..a0affc9ee2 100644 Binary files a/web/static/images/icons/patch.png and b/web/static/images/icons/patch.png differ diff --git a/web/static/images/icons/pdf.png b/web/static/images/icons/pdf.png index e4582570e5..8c7507a1c9 100644 Binary files a/web/static/images/icons/pdf.png and b/web/static/images/icons/pdf.png differ diff --git a/web/static/images/icons/ppt.png b/web/static/images/icons/ppt.png index 3571b46491..51553a11cc 100644 Binary files a/web/static/images/icons/ppt.png and b/web/static/images/icons/ppt.png differ diff --git a/web/static/images/icons/video.png b/web/static/images/icons/video.png index e61a9e5f46..f53da93e4c 100644 Binary files a/web/static/images/icons/video.png and b/web/static/images/icons/video.png differ diff --git a/web/static/images/icons/word.png b/web/static/images/icons/word.png index 20f8306656..6589378174 100644 Binary files a/web/static/images/icons/word.png and b/web/static/images/icons/word.png differ