diff options
32 files changed, 545 insertions, 396 deletions
diff --git a/api/file.go b/api/file.go index 3ef50fbbd..219cf6103 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/api/user.go b/api/user.go index 5d6e649cb..e2d80b366 100644 --- a/api/user.go +++ b/api/user.go @@ -195,7 +195,7 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { l4g.Error("Failed to set email verified err=%v", cresult.Err) } } else { - FireAndForgetVerifyEmail(result.Data.(*model.User).Id, ruser.FirstName, ruser.Email, team.DisplayName, c.GetTeamURLFromTeam(team)) + FireAndForgetVerifyEmail(result.Data.(*model.User).Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) } ruser.Sanitize(map[string]bool{}) @@ -225,19 +225,18 @@ func fireAndForgetWelcomeEmail(name, email, teamDisplayName, link string) { }() } -func FireAndForgetVerifyEmail(userId, name, email, teamDisplayName, teamURL string) { +func FireAndForgetVerifyEmail(userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) { go func() { - link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s", teamURL, userId, model.HashPassword(userId)) + link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail) subjectPage := NewServerTemplatePage("verify_subject", teamURL) subjectPage.Props["TeamDisplayName"] = teamDisplayName bodyPage := NewServerTemplatePage("verify_body", teamURL) - bodyPage.Props["Nickname"] = name bodyPage.Props["TeamDisplayName"] = teamDisplayName bodyPage.Props["VerifyUrl"] = link - if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { + if err := utils.SendMail(userEmail, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error("Failed to send verification email successfully err=%v", err) } }() diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index 6cc8ec8a9..16768a119 100644 --- a/web/react/components/access_history_modal.jsx +++ b/web/react/components/access_history_modal.jsx @@ -15,13 +15,13 @@ function getStateFromStoresForAudits() { module.exports = React.createClass({ componentDidMount: function() { UserStore.addAuditsChangeListener(this._onChange); - $(this.refs.modal.getDOMNode()).on('shown.bs.modal', function (e) { + $(this.refs.modal.getDOMNode()).on('shown.bs.modal', function(e) { AsyncClient.getAudits(); }); var self = this; $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) { - self.setState({ moreInfo: [] }); + self.setState({moreInfo: []}); }); }, componentWillUnmount: function() { @@ -36,7 +36,7 @@ module.exports = React.createClass({ handleMoreInfo: function(index) { var newMoreInfo = this.state.moreInfo; newMoreInfo[index] = true; - this.setState({ moreInfo: newMoreInfo }); + this.setState({moreInfo: newMoreInfo}); }, getInitialState: function() { var initialState = getStateFromStoresForAudits(); @@ -57,24 +57,28 @@ module.exports = React.createClass({ newDate = (<div> {currentHistoryDate.toDateString()} </div>); } + if (!currentAudit.session_id && currentAudit.action.search('/users/login') !== -1) { + currentAudit.session_id = 'N/A (Login attempt)'; + } + accessList[i] = ( - <div className="access-history__table"> - <div className="access__date">{newDate}</div> - <div className="access__report"> - <div className="report__time">{newHistoryDate.toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit'})}</div> - <div className="report__info"> - <div>{"IP: " + currentAudit.ip_address}</div> - { this.state.moreInfo[i] ? + <div className='access-history__table'> + <div className='access__date'>{newDate}</div> + <div className='access__report'> + <div className='report__time'>{newHistoryDate.toLocaleTimeString(navigator.language, {hour: '2-digit', minute: '2-digit'})}</div> + <div className='report__info'> + <div>{'IP: ' + currentAudit.ip_address}</div> + {this.state.moreInfo[i] ? <div> - <div>{"Session ID: " + currentAudit.session_id}</div> - <div>{"URL: " + currentAudit.action.replace("/api/v1", "")}</div> + <div>{'Session ID: ' + currentAudit.session_id}</div> + <div>{'URL: ' + currentAudit.action.replace(/\/api\/v[1-9]/, '')}</div> </div> : - <a href="#" className="theme" onClick={this.handleMoreInfo.bind(this, i)}>More info</a> + <a href='#' className='theme' onClick={this.handleMoreInfo.bind(this, i)}>More info</a> } </div> {i < this.state.audits.length - 1 ? - <div className="divider-light"/> + <div className='divider-light'/> : null } @@ -85,17 +89,17 @@ module.exports = React.createClass({ return ( <div> - <div className="modal fade" ref="modal" id="access-history" tabIndex="-1" role="dialog" aria-hidden="true"> - <div className="modal-dialog modal-lg"> - <div className="modal-content"> - <div className="modal-header"> - <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> - <h4 className="modal-title" id="myModalLabel">Access History</h4> + <div className='modal fade' ref='modal' id='access-history' tabIndex='-1' role='dialog' aria-hidden='true'> + <div className='modal-dialog modal-lg'> + <div className='modal-content'> + <div className='modal-header'> + <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button> + <h4 className='modal-title' id='myModalLabel'>Access History</h4> </div> - <div ref="modalBody" className="modal-body"> - { !this.state.audits.loading ? - <form role="form"> - { accessList } + <div ref='modalBody' className='modal-body'> + {!this.state.audits.loading ? + <form role='form'> + {accessList} </form> : <LoadingScreen /> diff --git a/web/react/components/delete_channel_modal.jsx b/web/react/components/delete_channel_modal.jsx index e23a37740..64ceec450 100644 --- a/web/react/components/delete_channel_modal.jsx +++ b/web/react/components/delete_channel_modal.jsx @@ -47,7 +47,7 @@ module.exports = React.createClass({ </p> </div> <div className="modal-footer"> - <button type="button" className="btn btn-default" data-dismiss="modal">Close</button> + <button type="button" className="btn btn-default" data-dismiss="modal">Cancel</button> <button type="button" className="btn btn-danger" data-dismiss="modal" onClick={this.handleDelete}>Delete</button> </div> </div> diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx index a35a531b5..1b0cc185f 100644 --- a/web/react/components/edit_channel_modal.jsx +++ b/web/react/components/edit_channel_modal.jsx @@ -63,7 +63,7 @@ module.exports = React.createClass({ { server_error } </div> <div className="modal-footer"> - <button type="button" className="btn btn-default" data-dismiss="modal">Close</button> + <button type="button" className="btn btn-default" data-dismiss="modal">Cancel</button> <button type="button" className="btn btn-primary" onClick={this.handleEdit}>Save</button> </div> </div> diff --git a/web/react/components/email_verify.jsx b/web/react/components/email_verify.jsx index 168608274..678eb9928 100644 --- a/web/react/components/email_verify.jsx +++ b/web/react/components/email_verify.jsx @@ -11,7 +11,7 @@ module.exports = React.createClass({ var resend = ""; if (this.props.isVerified === "true") { title = config.SiteName + " Email Verified"; - body = <p>Your email has been verified! Click <a href="/">here</a> to log in.</p>; + body = <p>Your email has been verified! Click <a href={this.props.teamURL + "?email=" + this.props.userEmail}>here</a> to log in.</p>; } else { title = config.SiteName + " Email Not Verified"; body = <p>Please verify your email address. Check your inbox for an email.</p>; diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx new file mode 100644 index 000000000..3cd791887 --- /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; + $('<img/>').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 = <div ref={filename} className="post__load" style={{backgroundImage: 'url(/static/images/load.gif)'}}/>; + } else { + thumbnail = <div className={"file-icon "+utils.getIconClassName(type)}/>; + } + + 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 ( + <div className="post-image__column" key={filename}> + <a className="post-image__thumbnail" href="#" onClick={this.props.handleImageClick} + data-img-id={this.props.index} data-toggle="modal" data-target={"#" + this.props.modalId }> + {thumbnail} + </a> + <div className="post-image__details"> + <div className="post-image__name">{decodeURIComponent(utils.getFileName(filename))}</div> + <div> + <span className="post-image__type">{fileInfo.ext.toUpperCase()}</span> + <span className="post-image__size">{fileSizeString}</span> + </div> + </div> + </div> + ); + } +}); diff --git a/web/react/components/file_attachment_list.jsx b/web/react/components/file_attachment_list.jsx new file mode 100644 index 000000000..b92442957 --- /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(<FileAttachment key={i} filenames={filenames} index={i} modalId={modalId} handleImageClick={this.handleImageClick} />); + } + + return ( + <div> + <div className="post-image__columns"> + {postFiles} + </div> + <ViewImageModal + channelId={this.props.channelId} + userId={this.props.userId} + modalId={modalId} + startId={this.state.startImgId} + filenames={filenames} /> + </div> + ); + }, + handleImageClick: function(e) { + this.setState({startImgId: parseInt($(e.target.parentNode).attr('data-img-id'))}); + } +}); diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index 94be2acd6..fed96b50a 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -212,7 +212,7 @@ module.exports = React.createClass({ <span>People invited automatically join Town Square channel.</span> </div> <div className="modal-footer"> - <button type="button" className="btn btn-default" data-dismiss="modal">Close</button> + <button type="button" className="btn btn-default" data-dismiss="modal">Cancel</button> <button onClick={this.handleSubmit} type="button" className="btn btn-primary">Send Invitations</button> </div> </div> diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index 641ffeef2..860c96d84 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") { - $('<img/>').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( - <div className="post-image__column" key={filenames[i]}> - <a href="#" onClick={this.handleImageClick} data-img-id={images.length.toString()} data-toggle="modal" data-target={"#" + postImageModalId }><div ref={filenames[i]} className="post__load" style={{backgroundImage: 'url(/static/images/load.gif)'}}></div></a> - </div> - ); - } - images.push(filenames[i]); - } else if (i < Constants.MAX_DISPLAY_FILES) { - postFiles.push( - <div className="post-image__column custom-file" key={fileInfo.name+i}> - <a href={fileInfo.path + (fileInfo.ext ? "." + fileInfo.ext : "")} download={fileInfo.name + (fileInfo.ext ? "." + fileInfo.ext : "")}> - <div className={"file-icon "+utils.getIconClassName(type)}/> - </a> - </div> - ); - } - } - } - 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 } <p key={post.id+"_message"} className={postClass}><span>{inner}</span></p> { filenames && filenames.length > 0 ? - <div className="post-image__columns"> - { postFiles } - </div> - : "" } - { embed } - - { images.length > 0 ? - <ViewImageModal + <FileAttachmentList + filenames={filenames} + modalId={"view_image_modal_" + post.id} channelId={post.channel_id} - userId={post.user_id} - modalId={postImageModalId} - startId={this.state.startImgId} - imgCount={post.img_count} - filenames={images} /> + userId={post.user_id} /> : "" } + { embed } </div> ); } diff --git a/web/react/components/post_right.jsx b/web/react/components/post_right.jsx index 8097a181e..ad8b54012 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( - <div className="post-image__column" key={fileInfo.path}> - <a href="#" onClick={this.handleImageClick} data-img-id={images.length.toString()} data-toggle="modal" data-target={"#" + postImageModalId }><div ref={fileInfo.path} className="post__image" style={{backgroundImage: 'url(' + url + '_thumb.jpg)'}}></div></a> - </div> - ); - images.push(filenames[i]); - } else { - postFiles.push( - <div className="post-image__column custom-file" key={fileInfo.path}> - <a href={fileInfo.path+"."+fileInfo.ext} download={fileInfo.name+"."+fileInfo.ext}> - <div className={"file-icon "+utils.getIconClassName(ftype)}/> - </a> - </div> - ); - } - } - } - return ( <div className={"post post--root " + currentUserCss}> <div className="post-right-channel__name">{ channelName }</div> <div className="post-profile-img__container"> - <img className="post-profile-img" src={"/api/v1/users/" + this.props.post.user_id + "/image?time=" + timestamp} height="36" width="36" /> + <img className="post-profile-img" src={"/api/v1/users/" + post.user_id + "/image?time=" + timestamp} height="36" width="36" /> </div> <div className="post__content"> <ul className="post-header"> - <li className="post-header-col"><strong><UserProfile userId={this.props.post.user_id} /></strong></li> - <li className="post-header-col"><time className="post-right-root-time">{ utils.displayDate(this.props.post.create_at)+' '+utils.displayTime(this.props.post.create_at) }</time></li> + <li className="post-header-col"><strong><UserProfile userId={post.user_id} /></strong></li> + <li className="post-header-col"><time className="post-right-root-time">{ utils.displayDate(post.create_at)+' '+utils.displayTime(post.create_at) }</time></li> <li className="post-header-col post-header__reply"> <div className="dropdown"> { isOwner ? <div> <a href="#" className="dropdown-toggle theme" type="button" data-toggle="dropdown" aria-expanded="false" /> <ul className="dropdown-menu" role="menu"> - <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#edit_post" data-title={type} data-message={this.props.post.message} data-postid={this.props.post.id} data-channelid={this.props.post.channel_id}>Edit</a></li> - <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#delete_post" data-title={type} data-postid={this.props.post.id} data-channelid={this.props.post.channel_id} data-comments={this.props.commentCount}>Delete</a></li> + <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#edit_post" data-title={type} data-message={post.message} data-postid={post.id} data-channelid={post.channel_id}>Edit</a></li> + <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#delete_post" data-title={type} data-postid={post.id} data-channelid={post.channel_id} data-comments={this.props.commentCount}>Delete</a></li> </ul> </div> : "" } @@ -146,19 +102,12 @@ RootPost = React.createClass({ </ul> <div className="post-body"> <p>{message}</p> - { filenames.length > 0 ? - <div className="post-image__columns"> - { postFiles } - </div> - : "" } - { images.length > 0 ? - <ViewImageModal - channelId={this.props.post.channel_id} - userId={this.props.post.user_id} - modalId={postImageModalId} - startId={this.state.startImgId} - imgCount={this.props.post.img_count} - filenames={images} /> + { post.filenames && post.filenames.length > 0 ? + <FileAttachmentList + filenames={post.filenames} + modalId={"rhs_view_image_modal_" + post.id} + channelId={post.channel_id} + userId={post.user_id} /> : "" } </div> </div> @@ -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( - <div className="post-image__column" key={fileInfo.path}> - <a href="#" onClick={this.handleImageClick} data-img-id={images.length.toString()} data-toggle="modal" data-target={"#" + postImageModalId }><div ref={fileInfo.path} className="post__image" style={{backgroundImage: 'url(' + url + '_thumb.jpg)'}}></div></a> - </div> - ); - images.push(filenames[i]); - } else { - postFiles.push( - <div className="post-image__column custom-file" key={fileInfo.path}> - <a href={fileInfo.path+"."+fileInfo.ext} download={fileInfo.name+"."+fileInfo.ext}> - <div className={"file-icon "+utils.getIconClassName(type)}/> - </a> - </div> - ); - } - } - } - - var message = utils.textToJsx(this.props.post.message); + var message = utils.textToJsx(post.message); var timestamp = UserStore.getCurrentUser().update_at; return ( <div className={commentClass + " " + currentUserCss}> <div className="post-profile-img__container"> - <img className="post-profile-img" src={"/api/v1/users/" + this.props.post.user_id + "/image?time=" + timestamp} height="36" width="36" /> + <img className="post-profile-img" src={"/api/v1/users/" + post.user_id + "/image?time=" + timestamp} height="36" width="36" /> </div> <div className="post__content"> <ul className="post-header"> - <li className="post-header-col"><strong><UserProfile userId={this.props.post.user_id} /></strong></li> - <li className="post-header-col"><time className="post-right-comment-time">{ utils.displayDateTime(this.props.post.create_at) }</time></li> + <li className="post-header-col"><strong><UserProfile userId={post.user_id} /></strong></li> + <li className="post-header-col"><time className="post-right-comment-time">{ utils.displayDateTime(post.create_at) }</time></li> <li className="post-header-col post-header__reply"> { isOwner ? <div className="dropdown" onClick={function(e){$('.post-list-holder-by-time').scrollTop($(".post-list-holder-by-time").scrollTop() + 50);}}> <a href="#" className="dropdown-toggle theme" type="button" data-toggle="dropdown" aria-expanded="false" /> <ul className="dropdown-menu" role="menu"> - <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#edit_post" data-title={type} data-message={this.props.post.message} data-postid={this.props.post.id} data-channelid={this.props.post.channel_id}>Edit</a></li> - <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#delete_post" data-title={type} data-postid={this.props.post.id} data-channelid={this.props.post.channel_id} data-comments={0}>Delete</a></li> + <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#edit_post" data-title={type} data-message={post.message} data-postid={post.id} data-channelid={post.channel_id}>Edit</a></li> + <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#delete_post" data-title={type} data-postid={post.id} data-channelid={post.channel_id} data-comments={0}>Delete</a></li> </ul> </div> : "" } @@ -256,19 +161,12 @@ CommentPost = React.createClass({ </ul> <div className="post-body"> <p>{message}</p> - { filenames.length > 0 ? - <div className="post-image__columns"> - { postFiles } - </div> - : "" } - { images.length > 0 ? - <ViewImageModal - channelId={this.props.post.channel_id} - userId={this.props.post.user_id} - modalId={postImageModalId} - startId={this.state.startImgId} - imgCount={this.props.post.img_count} - filenames={images} /> + { post.filenames && post.filenames.length > 0 ? + <FileAttachmentList + filenames={post.filenames} + modalId={"rhs_comment_view_image_modal_" + post.id} + channelId={post.channel_id} + userId={post.user_id} /> : "" } </div> </div> diff --git a/web/react/components/rename_channel_modal.jsx b/web/react/components/rename_channel_modal.jsx index d67ab3afe..26593b7fa 100644 --- a/web/react/components/rename_channel_modal.jsx +++ b/web/react/components/rename_channel_modal.jsx @@ -139,7 +139,7 @@ module.exports = React.createClass({ </form> </div> <div className="modal-footer"> - <button type="button" className="btn btn-default" data-dismiss="modal">Close</button> + <button type="button" className="btn btn-default" data-dismiss="modal">Cancel</button> <button onClick={this.handleSubmit} type="button" className="btn btn-primary">Save</button> </div> </div> diff --git a/web/react/components/rename_team_modal.jsx b/web/react/components/rename_team_modal.jsx index dfd775a3b..bebdd6662 100644 --- a/web/react/components/rename_team_modal.jsx +++ b/web/react/components/rename_team_modal.jsx @@ -83,7 +83,7 @@ module.exports = React.createClass({ </form> </div> <div className="modal-footer"> - <button type="button" className="btn btn-default" data-dismiss="modal">Close</button> + <button type="button" className="btn btn-default" data-dismiss="modal">Cancel</button> <button onClick={this.handleSubmit} type="button" className="btn btn-primary">Save</button> </div> </div> diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index 670aab943..03808e821 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -58,7 +58,7 @@ module.exports = React.createClass({ }.bind(this), function(err) { if (err.message == "Login failed because email address has not been verified") { - window.location.href = "/verify_email?email="+ encodeURIComponent(this.state.user.email) + "&domain=" + encodeURIComponent(this.props.teamName); + window.location.href = "/verify_email?email="+ encodeURIComponent(this.state.user.email) + "&teamname=" + encodeURIComponent(this.props.teamName); } else { this.state.server_error = err.message; this.setState(this.state); @@ -107,7 +107,7 @@ module.exports = React.createClass({ <div className={ this.state.original_email == "" ? "margin--extra" : "hidden"} > <h5><strong>What's your email address?</strong></h5> <div className={ email_error ? "form-group has-error" : "form-group" }> - <input type="email" ref="email" className="form-control" defaultValue={ this.state.user.email } placeholder="" maxLength="128" /> + <input type="email" ref="email" className="form-control" defaultValue={ this.state.user.email } placeholder="" maxLength="128" autoFocus={true} /> { email_error } </div> </div> @@ -123,6 +123,7 @@ module.exports = React.createClass({ return ( <div> + <form> <img className="signup-team-logo" src="/static/images/logo.png" /> <h5 className="margin--less">Welcome to:</h5> <h2 className="signup-team__name">{ this.props.teamDisplayName }</h2> @@ -148,9 +149,10 @@ module.exports = React.createClass({ </div> </div> </div> - <p className="margin--extra"><button onClick={this.handleSubmit} className="btn-primary btn">Create Account</button></p> + <p className="margin--extra"><button type='submit' onClick={this.handleSubmit} className="btn-primary btn">Create Account</button></p> { server_error } <p>By creating an account and using Mattermost you are agreeing to our <a href={ config.TermsLink }>Terms of Service</a>. If you do not agree, you cannot use this service.</p> + </form> </div> ); } diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx index 7b096c629..4b2f8f650 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]; + var filename = this.props.filenames[id]; + + 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 { - 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'; + // there's nothing to load for non-image files + var loaded = this.state.loaded; + loaded[id] = true; + this.setState({ loaded: loaded }); } - - 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 }); }, 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 <div/>; } - 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 = ( + <a href={fileUrl} target="_blank"> + <img ref="image" src={this.getPreviewImagePath(filename)}/> + </a> + ); + } 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 = ( + <div className="file-details__container"> + <a className={"file-details__preview"} href={fileUrl} target="_blank"> + <span className="file-details__preview-helper" /> + <img ref="image" src={this.getPreviewImagePath(filename)} /> + </a> + <div className="file-details"> + <div className="file-details__name">{name}</div> + <div className="file-details__info">{infoString}</div> + </div> + </div> + ); + + // 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 = ( - <div key={name+"_loading"}> - <img ref="placeholder" className="loader-image" src="/static/images/load.gif" /> + content = ( + <div> + <img className="loader-image" src="/static/images/load.gif" /> { percentage > 0 ? <span className="loader-percent" >{"Previewing " + percentage + "%"}</span> : ""} </div> ); 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']] = <a key={info['path']} className={imgClass} href={info.path+"."+info.ext} target="_blank"><img ref="image" src={preview_filename}/></a>; - } } - 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 ( <div className="modal fade image_modal" ref="modal" id={this.props.modalId} tabIndex="-1" role="dialog" aria-hidden="true"> <div className="modal-dialog modal-image"> @@ -175,7 +198,7 @@ module.exports = React.createClass({ <div ref="imageBody" className="modal-body image-body"> <div ref="imageWrap" className={"image-wrapper default " + bgClass}> <div className="modal-close" data-dismiss="modal"></div> - {imgFragment} + {content} <div ref="imageFooter" className="modal-button-bar"> <span className="pull-left text">{"Image "+(this.state.imgId+1)+" of "+this.props.filenames.length}</span> <div className="image-links"> @@ -185,10 +208,9 @@ module.exports = React.createClass({ <span className="text"> | </span> </div> : "" } - <a href={download_link} download={decodeURIComponent(name)} className="text">Download</a> + <a href={fileUrl} download={name} className="text">Download</a> </div> </div> - {loading} </div> { this.props.filenames.length > 1 ? <a className="modal-prev-bar" href="#" onClick={this.handlePrev}> @@ -205,5 +227,23 @@ module.exports = React.createClass({ </div> </div> ); + }, + // 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/pages/verify.jsx b/web/react/pages/verify.jsx index 69850849f..96b556983 100644 --- a/web/react/pages/verify.jsx +++ b/web/react/pages/verify.jsx @@ -3,11 +3,9 @@ var EmailVerify = require('../components/email_verify.jsx'); -global.window.setup_verify_page = function(is_verified) { - +global.window.setupVerifyPage = function setupVerifyPage(isVerified, teamURL, userEmail) { React.render( - <EmailVerify isVerified={is_verified} />, + <EmailVerify isVerified={isVerified} teamURL={teamURL} userEmail={userEmail} />, document.getElementById('verify') ); - }; diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 8a4d92b85..09240bf06 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 56d03e171..ea7548267 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/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index 81b94ab5a..e3f140413 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -427,9 +427,9 @@ body { &.white { .inner__wrap { - >.row.content { - margin-bottom: -185px; - } + >.row.content { + margin-bottom: -185px; + } } } } @@ -447,6 +447,9 @@ } } } + .search__clear { + display: block; + } .search-bar__container { padding: 0; height: 45px; @@ -457,15 +460,17 @@ @include translateX(-45px); } .search__form { - padding-left: 10px; - padding-right: 67px; + @include translateX(-45px); + padding-left: 55px; + padding-right: 24px; } .search__clear { - display: block; + @include translateX(0px); } } .search__form { border: none; + @include translateX(0px); padding: 7px 20px 0 49px; height: 45px; position: relative; diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss index e2168ef75..9ae41ebb0 100644 --- a/web/sass-files/sass/partials/_search.scss +++ b/web/sass-files/sass/partials/_search.scss @@ -7,6 +7,8 @@ right: 0; line-height: 45px; margin-right: 13px; + @include single-transition(all, 0.2s, linear); + @include translateX(60px); z-index: 5; cursor: pointer; } diff --git a/web/static/images/icons/audio.png b/web/static/images/icons/audio.png Binary files differindex 2b6d37f8d..bd25b7f84 100644 --- a/web/static/images/icons/audio.png +++ b/web/static/images/icons/audio.png diff --git a/web/static/images/icons/code.png b/web/static/images/icons/code.png Binary files differindex 80db302ee..c59e4b8dc 100644 --- a/web/static/images/icons/code.png +++ b/web/static/images/icons/code.png diff --git a/web/static/images/icons/excel.png b/web/static/images/icons/excel.png Binary files differindex 70ddadcbf..275c65c4d 100644 --- a/web/static/images/icons/excel.png +++ b/web/static/images/icons/excel.png diff --git a/web/static/images/icons/generic.png b/web/static/images/icons/generic.png Binary files differindex d9e82c232..0eb82c2d2 100644 --- a/web/static/images/icons/generic.png +++ b/web/static/images/icons/generic.png diff --git a/web/static/images/icons/image.png b/web/static/images/icons/image.png Binary files differindex a3acdef4c..799317731 100644 --- a/web/static/images/icons/image.png +++ b/web/static/images/icons/image.png diff --git a/web/static/images/icons/patch.png b/web/static/images/icons/patch.png Binary files differindex 18af126d4..a0affc9ee 100644 --- a/web/static/images/icons/patch.png +++ b/web/static/images/icons/patch.png diff --git a/web/static/images/icons/pdf.png b/web/static/images/icons/pdf.png Binary files differindex e4582570e..8c7507a1c 100644 --- a/web/static/images/icons/pdf.png +++ b/web/static/images/icons/pdf.png diff --git a/web/static/images/icons/ppt.png b/web/static/images/icons/ppt.png Binary files differindex 3571b4649..51553a11c 100644 --- a/web/static/images/icons/ppt.png +++ b/web/static/images/icons/ppt.png diff --git a/web/static/images/icons/video.png b/web/static/images/icons/video.png Binary files differindex e61a9e5f4..f53da93e4 100644 --- a/web/static/images/icons/video.png +++ b/web/static/images/icons/video.png diff --git a/web/static/images/icons/word.png b/web/static/images/icons/word.png Binary files differindex 20f830665..658937817 100644 --- a/web/static/images/icons/word.png +++ b/web/static/images/icons/word.png diff --git a/web/templates/verify.html b/web/templates/verify.html index a61964bb3..de839db68 100644 --- a/web/templates/verify.html +++ b/web/templates/verify.html @@ -9,7 +9,7 @@ </div> </div> <script> - window.setup_verify_page('{{ .Props.IsVerified }}'); + window.setupVerifyPage('{{.Props.IsVerified}}', '{{.Props.TeamURL}}', '{{.Props.UserEmail}}'); </script> </body> </html> diff --git a/web/web.go b/web/web.go index 68e2a5226..8b329c149 100644 --- a/web/web.go +++ b/web/web.go @@ -352,27 +352,26 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { resend := r.URL.Query().Get("resend") - name := r.URL.Query().Get("name") + name := r.URL.Query().Get("teamname") email := r.URL.Query().Get("email") hashedId := r.URL.Query().Get("hid") userId := r.URL.Query().Get("uid") - if resend == "true" { - - teamId := "" - if result := <-api.Srv.Store.Team().GetByName(name); result.Err != nil { - c.Err = result.Err - return - } else { - teamId = result.Data.(*model.Team).Id - } + var team *model.Team + if result := <-api.Srv.Store.Team().GetByName(name); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } - if result := <-api.Srv.Store.User().GetByEmail(teamId, email); result.Err != nil { + if resend == "true" { + if result := <-api.Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { c.Err = result.Err return } else { user := result.Data.(*model.User) - api.FireAndForgetVerifyEmail(user.Id, strings.Split(user.Nickname, " ")[0], user.Email, name, c.GetTeamURL()) + api.FireAndForgetVerifyEmail(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) http.Redirect(w, r, "/", http.StatusFound) return } @@ -396,6 +395,8 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { page := NewHtmlTemplatePage("verify", "Email Verified") page.Props["IsVerified"] = isVerified + page.Props["TeamURL"] = c.GetTeamURLFromTeam(team) + page.Props["UserEmail"] = email page.Render(c, w) } |