summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md4
-rw-r--r--api/file.go7
-rw-r--r--api/team.go21
-rw-r--r--api/user.go11
-rw-r--r--web/react/components/access_history_modal.jsx52
-rw-r--r--web/react/components/delete_channel_modal.jsx2
-rw-r--r--web/react/components/edit_channel_modal.jsx2
-rw-r--r--web/react/components/email_verify.jsx2
-rw-r--r--web/react/components/file_attachment.jsx121
-rw-r--r--web/react/components/file_attachment_list.jsx49
-rw-r--r--web/react/components/invite_member_modal.jsx2
-rw-r--r--web/react/components/post_body.jsx100
-rw-r--r--web/react/components/post_right.jsx172
-rw-r--r--web/react/components/rename_channel_modal.jsx2
-rw-r--r--web/react/components/rename_team_modal.jsx2
-rw-r--r--web/react/components/sidebar_header.jsx19
-rw-r--r--web/react/components/signup_user_complete.jsx8
-rw-r--r--web/react/components/user_settings.jsx203
-rw-r--r--web/react/components/view_image.jsx186
-rw-r--r--web/react/pages/verify.jsx6
-rw-r--r--web/react/utils/utils.jsx65
-rw-r--r--web/sass-files/sass/partials/_files.scss108
-rw-r--r--web/sass-files/sass/partials/_responsive.scss17
-rw-r--r--web/sass-files/sass/partials/_search.scss2
-rw-r--r--web/static/images/icons/audio.pngbin7432 -> 4859 bytes
-rw-r--r--web/static/images/icons/code.pngbin7195 -> 4669 bytes
-rw-r--r--web/static/images/icons/excel.pngbin6209 -> 3648 bytes
-rw-r--r--web/static/images/icons/generic.pngbin8894 -> 6258 bytes
-rw-r--r--web/static/images/icons/image.pngbin5604 -> 3995 bytes
-rw-r--r--web/static/images/icons/patch.pngbin7865 -> 4956 bytes
-rw-r--r--web/static/images/icons/pdf.pngbin11451 -> 5683 bytes
-rw-r--r--web/static/images/icons/ppt.pngbin8450 -> 5588 bytes
-rw-r--r--web/static/images/icons/video.pngbin5300 -> 3593 bytes
-rw-r--r--web/static/images/icons/word.pngbin4543 -> 3674 bytes
-rw-r--r--web/templates/verify.html2
-rw-r--r--web/web.go25
36 files changed, 693 insertions, 497 deletions
diff --git a/README.md b/README.md
index f0857e99d..4ba3de128 100644
--- a/README.md
+++ b/README.md
@@ -19,8 +19,8 @@ Learn More
<li/>Make a pull request: http://www.mattermost.org/contribute-to-mattermost/</li>
</ul>
-Installing the Mattermost
-=========================
+Installing Mattermost
+=====================
You're installing "Mattermost Alpha", a pre-released version intended for an early look at what we're building. While SpinPunch runs this version internally, it's not recommended for production deployments since we can't guarantee API stability or backwards compatibility until our production release.
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/team.go b/api/team.go
index c9fe42ecc..01c8e50b6 100644
--- a/api/team.go
+++ b/api/team.go
@@ -275,11 +275,24 @@ func emailTeams(c *Context, w http.ResponseWriter, r *http.Request) {
subjectPage := NewServerTemplatePage("find_teams_subject", c.GetSiteURL())
bodyPage := NewServerTemplatePage("find_teams_body", c.GetSiteURL())
- if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil {
- l4g.Error("An error occured while sending an email in emailTeams err=%v", err)
- }
+ if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil {
+ c.Err = result.Err
+ } else {
+ teams := result.Data.([]*model.Team)
- w.Write([]byte(model.MapToJson(m)))
+ // the template expects Props to be a map with team names as the keys
+ props := make(map[string]string)
+ for _, team := range teams {
+ props[team.Name] = team.Name
+ }
+ bodyPage.Props = props
+
+ if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil {
+ l4g.Error("An error occured while sending an email in emailTeams err=%v", err)
+ }
+
+ w.Write([]byte(model.MapToJson(m)))
+ }
}
func inviteMembers(c *Context, w http.ResponseWriter, r *http.Request) {
diff --git a/api/user.go b/api/user.go
index 5d6e649cb..66527ca1a 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)
}
}()
@@ -864,7 +863,7 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) {
}
if !model.ComparePassword(user.Password, currentPassword) {
- c.Err = model.NewAppError("updatePassword", "Update password failed because of invalid password", "")
+ c.Err = model.NewAppError("updatePassword", "The \"Current Password\" you entered is incorrect. Please check that Caps Lock is off and try again.", "")
c.Err.StatusCode = http.StatusForbidden
return
}
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">&times;</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'>&times;</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/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index 0156dc01a..e01ddcd05 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -17,11 +17,20 @@ var NavbarDropdown = React.createClass({
e.preventDefault();
client.logout();
},
+ blockToggle: false,
componentDidMount: function() {
UserStore.addTeamsChangeListener(this._onChange);
+
+ var self = this;
+ $(this.refs.dropdown.getDOMNode()).on('hide.bs.dropdown', function(e) {
+ self.blockToggle = true;
+ setTimeout(function(){self.blockToggle = false;}, 100);
+ });
},
componentWillUnmount: function() {
UserStore.removeTeamsChangeListener(this._onChange);
+
+ $(this.refs.dropdown.getDOMNode()).off('hide.bs.dropdown');
},
_onChange: function() {
if (this.isMounted()) {
@@ -75,7 +84,7 @@ var NavbarDropdown = React.createClass({
return (
<ul className="nav navbar-nav navbar-right">
- <li className="dropdown">
+ <li ref="dropdown" className="dropdown">
<a href="#" className="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
<span className="dropdown__icon" dangerouslySetInnerHTML={{__html: Constants.MENU_ICON }} />
</a>
@@ -108,7 +117,11 @@ module.exports = React.createClass({
},
toggleDropdown: function(e) {
- $('.team__header').find('.dropdown-toggle').trigger('click');
+ if (this.refs.dropdown.blockToggle) {
+ this.refs.dropdown.blockToggle = false;
+ return;
+ }
+ $('.team__header').find('.dropdown-toggle').dropdown('toggle');
},
render: function() {
@@ -131,7 +144,7 @@ module.exports = React.createClass({
<div className="team__name">{ this.props.teamDisplayName }</div>
</div>
</a>
- <NavbarDropdown teamType={this.props.teamType} />
+ <NavbarDropdown ref="dropdown" teamType={this.props.teamType} />
</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/user_settings.jsx b/web/react/components/user_settings.jsx
index 902989b7b..95d1178d1 100644
--- a/web/react/components/user_settings.jsx
+++ b/web/react/components/user_settings.jsx
@@ -638,8 +638,8 @@ var GeneralTab = React.createClass({
var username = this.state.username.trim();
var username_error = utils.isValidUsername(username);
- if (username_error === "Cannot use a reserved word as a username.") {
- this.setState({client_error: "This username is reserved, please choose a new one." });
+ if (username_error === 'Cannot use a reserved word as a username.') {
+ this.setState({client_error: 'This username is reserved, please choose a new one.' });
return;
} else if (username_error) {
this.setState({client_error: "Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'." });
@@ -647,7 +647,7 @@ var GeneralTab = React.createClass({
}
if (user.username === username) {
- this.setState({client_error: "You must submit a new username"});
+ this.setState({client_error: 'You must submit a new username'});
return;
}
@@ -662,7 +662,7 @@ var GeneralTab = React.createClass({
var nickname = this.state.nickname.trim();
if (user.nickname === nickname) {
- this.setState({client_error: "You must submit a new nickname"})
+ this.setState({client_error: 'You must submit a new nickname'})
return;
}
@@ -678,7 +678,7 @@ var GeneralTab = React.createClass({
var lastName = this.state.last_name.trim();
if (user.first_name === firstName && user.last_name === lastName) {
- this.setState({client_error: "You must submit a new first or last name"})
+ this.setState({client_error: 'You must submit a new first or last name'})
return;
}
@@ -698,7 +698,7 @@ var GeneralTab = React.createClass({
}
if (email === '' || !utils.isEmail(email)) {
- this.setState({ email_error: "Please enter a valid email address" });
+ this.setState({ email_error: 'Please enter a valid email address' });
return;
}
@@ -708,16 +708,16 @@ var GeneralTab = React.createClass({
},
submitUser: function(user) {
client.updateUser(user,
- function(data) {
- this.updateSection("");
+ function() {
+ this.updateSection('');
AsyncClient.getMe();
}.bind(this),
function(err) {
state = this.getInitialState();
- if(err.message) {
+ if (err.message) {
state.server_error = err.message;
} else {
- state.server_error = err
+ state.server_error = err;
}
this.setState(state);
}.bind(this)
@@ -726,22 +726,26 @@ var GeneralTab = React.createClass({
submitPicture: function(e) {
e.preventDefault();
- if (!this.state.picture) return;
+ if (!this.state.picture) {
+ return;
+ }
- if(!this.submitActive) return;
+ if (!this.submitActive) {
+ return;
+ }
var picture = this.state.picture;
- if(picture.type !== "image/jpeg" && picture.type !== "image/png") {
- this.setState({client_error: "Only JPG or PNG images may be used for profile pictures"});
+ if (picture.type !== 'image/jpeg' && picture.type !== 'image/png') {
+ this.setState({client_error: 'Only JPG or PNG images may be used for profile pictures'});
return;
}
- formData = new FormData();
+ var formData = new FormData();
formData.append('image', picture, picture.name);
client.uploadProfileImage(formData,
- function(data) {
+ function() {
this.submitActive = false;
AsyncClient.getMe();
window.location.reload();
@@ -754,39 +758,39 @@ var GeneralTab = React.createClass({
);
},
updateUsername: function(e) {
- this.setState({ username: e.target.value });
+ this.setState({username: e.target.value});
},
updateFirstName: function(e) {
- this.setState({ first_name: e.target.value });
+ this.setState({first_name: e.target.value});
},
updateLastName: function(e) {
- this.setState({ last_name: e.target.value});
+ this.setState({last_name: e.target.value});
},
updateNickname: function(e) {
this.setState({nickname: e.target.value});
},
updateEmail: function(e) {
- this.setState({ email: e.target.value});
+ this.setState({email: e.target.value});
},
updatePicture: function(e) {
if (e.target.files && e.target.files[0]) {
this.setState({ picture: e.target.files[0] });
this.submitActive = true;
- this.setState({client_error:null})
+ this.setState({client_error: null});
} else {
- this.setState({ picture: null });
+ this.setState({picture: null});
}
},
updateSection: function(section) {
- this.setState({client_error:""})
- this.submitActive = false
+ this.setState({client_error:''});
+ this.submitActive = false;
this.props.updateSection(section);
},
handleClose: function() {
- $(this.getDOMNode()).find(".form-control").each(function() {
- this.value = "";
+ $(this.getDOMNode()).find('.form-control').each(function() {
+ this.value = '';
});
this.setState(assign({}, this.getInitialState(), {client_error: null, server_error: null, email_error: null}));
@@ -812,43 +816,45 @@ var GeneralTab = React.createClass({
var nameSection;
var self = this;
+ var inputs = [];
if (this.props.activeSection === 'name') {
- var inputs = [];
-
inputs.push(
- <div className="form-group">
- <label className="col-sm-5 control-label">First Name</label>
- <div className="col-sm-7">
- <input className="form-control" type="text" onChange={this.updateFirstName} value={this.state.first_name}/>
+ <div className='form-group'>
+ <label className='col-sm-5 control-label'>First Name</label>
+ <div className='col-sm-7'>
+ <input className='form-control' type='text' onChange={this.updateFirstName} value={this.state.first_name}/>
</div>
</div>
);
inputs.push(
- <div className="form-group">
- <label className="col-sm-5 control-label">Last Name</label>
- <div className="col-sm-7">
- <input className="form-control" type="text" onChange={this.updateLastName} value={this.state.last_name}/>
+ <div className='form-group'>
+ <label className='col-sm-5 control-label'>Last Name</label>
+ <div className='col-sm-7'>
+ <input className='form-control' type='text' onChange={this.updateLastName} value={this.state.last_name}/>
</div>
</div>
);
nameSection = (
<SettingItemMax
- title="Full Name"
+ title='Full Name'
inputs={inputs}
submit={this.submitName}
server_error={server_error}
client_error={client_error}
- updateSection={function(e){self.updateSection("");e.preventDefault();}}
+ updateSection={function(e) {
+ self.updateSection('');
+ e.preventDefault();
+ }}
/>
);
} else {
- var full_name = "";
+ var full_name = '';
if (user.first_name && user.last_name) {
- full_name = user.first_name + " " + user.last_name;
+ full_name = user.first_name + ' ' + user.last_name;
} else if (user.first_name) {
full_name = user.first_name;
} else if (user.last_name) {
@@ -857,107 +863,119 @@ var GeneralTab = React.createClass({
nameSection = (
<SettingItemMin
- title="Full Name"
+ title='Full Name'
describe={full_name}
- updateSection={function(){self.updateSection("name");}}
+ updateSection={function() {
+ self.updateSection('name');
+ }}
/>
);
}
var nicknameSection;
if (this.props.activeSection === 'nickname') {
- var inputs = [];
inputs.push(
- <div className="form-group">
- <label className="col-sm-5 control-label">{utils.isMobile() ? "": "Nickname"}</label>
- <div className="col-sm-7">
- <input className="form-control" type="text" onChange={this.updateNickname} value={this.state.nickname}/>
+ <div className='form-group'>
+ <label className='col-sm-5 control-label'>{utils.isMobile() ? '' : 'Nickname'}</label>
+ <div className='col-sm-7'>
+ <input className='form-control' type='text' onChange={this.updateNickname} value={this.state.nickname}/>
</div>
</div>
);
nicknameSection = (
<SettingItemMax
- title="Nickname"
+ title='Nickname'
inputs={inputs}
submit={this.submitNickname}
server_error={server_error}
client_error={client_error}
- updateSection={function(e){self.updateSection("");e.preventDefault();}}
+ updateSection={function(e) {
+ self.updateSection('');
+ e.preventDefault();
+ }}
/>
);
} else {
nicknameSection = (
<SettingItemMin
- title="Nickname"
+ title='Nickname'
describe={UserStore.getCurrentUser().nickname}
- updateSection={function(){self.updateSection("nickname");}}
+ updateSection={function() {
+ self.updateSection('nickname');
+ }}
/>
);
}
var usernameSection;
if (this.props.activeSection === 'username') {
- var inputs = [];
-
inputs.push(
- <div className="form-group">
- <label className="col-sm-5 control-label">{utils.isMobile() ? "": "Username"}</label>
- <div className="col-sm-7">
- <input className="form-control" type="text" onChange={this.updateUsername} value={this.state.username}/>
+ <div className='form-group'>
+ <label className='col-sm-5 control-label'>{utils.isMobile() ? '': 'Username'}</label>
+ <div className='col-sm-7'>
+ <input className='form-control' type='text' onChange={this.updateUsername} value={this.state.username}/>
</div>
</div>
);
usernameSection = (
<SettingItemMax
- title="Username"
+ title='Username'
inputs={inputs}
submit={this.submitUsername}
server_error={server_error}
client_error={client_error}
- updateSection={function(e){self.updateSection("");e.preventDefault();}}
+ updateSection={function(e) {
+ self.updateSection('');
+ e.preventDefault();
+ }}
/>
);
} else {
usernameSection = (
<SettingItemMin
- title="Username"
+ title='Username'
describe={UserStore.getCurrentUser().username}
- updateSection={function(){self.updateSection("username");}}
+ updateSection={function() {
+ self.updateSection('username');
+ }}
/>
);
}
var emailSection;
if (this.props.activeSection === 'email') {
- var inputs = [];
-
inputs.push(
- <div className="form-group">
- <label className="col-sm-5 control-label">Primary Email</label>
- <div className="col-sm-7">
- <input className="form-control" type="text" onChange={this.updateEmail} value={this.state.email}/>
+ <div className='form-group'>
+ <label className='col-sm-5 control-label'>Primary Email</label>
+ <div className='col-sm-7'>
+ <input className='form-control' type='text' onChange={this.updateEmail} value={this.state.email}/>
</div>
</div>
);
emailSection = (
<SettingItemMax
- title="Email"
+ title='Email'
inputs={inputs}
submit={this.submitEmail}
server_error={server_error}
client_error={email_error}
- updateSection={function(e){self.updateSection("");e.preventDefault();}}
+ updateSection={function(e) {
+ self.updateSection('');
+ e.preventDefault();
+ }}
/>
);
} else {
emailSection = (
<SettingItemMin
- title="Email"
+ title='Email'
describe={UserStore.getCurrentUser().email}
- updateSection={function(){self.updateSection("email");}}
+ updateSection={function() {
+ self.updateSection('email');
+ }}
/>
);
}
@@ -966,57 +984,60 @@ var GeneralTab = React.createClass({
if (this.props.activeSection === 'picture') {
pictureSection = (
<SettingPicture
- title="Profile Picture"
+ title='Profile Picture'
submit={this.submitPicture}
- src={"/api/v1/users/" + user.id + "/image?time=" + user.last_picture_update}
+ src={'/api/v1/users/' + user.id + '/image?time=' + user.last_picture_update}
server_error={server_error}
client_error={client_error}
- updateSection={function(e){self.updateSection("");e.preventDefault();}}
+ updateSection={function(e) {
+ self.updateSection('');
+ e.preventDefault();
+ }}
picture={this.state.picture}
pictureChange={this.updatePicture}
submitActive={this.submitActive}
/>
);
-
} else {
- var minMessage = "Click Edit to upload an image.";
+ var minMessage = 'Click \'Edit\' to upload an image.';
if (user.last_picture_update) {
- minMessage = "Image last updated " + utils.displayDate(user.last_picture_update)
+ minMessage = 'Image last updated ' + utils.displayDate(user.last_picture_update);
}
pictureSection = (
<SettingItemMin
- title="Profile Picture"
+ title='Profile Picture'
describe={minMessage}
- updateSection={function(){self.updateSection("picture");}}
+ updateSection={function() {
+ self.updateSection('picture');
+ }}
/>
);
}
return (
<div>
- <div className="modal-header">
- <button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
- <h4 className="modal-title" ref="title"><i className="modal-back"></i>General Settings</h4>
+ <div className='modal-header'>
+ <button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>&times;</span></button>
+ <h4 className='modal-title' ref='title'><i className='modal-back'></i>General Settings</h4>
</div>
- <div className="user-settings">
- <h3 className="tab-header">General Settings</h3>
- <div className="divider-dark first"/>
+ <div className='user-settings'>
+ <h3 className='tab-header'>General Settings</h3>
+ <div className='divider-dark first'/>
{nameSection}
- <div className="divider-light"/>
+ <div className='divider-light'/>
{usernameSection}
- <div className="divider-light"/>
+ <div className='divider-light'/>
{nicknameSection}
- <div className="divider-light"/>
+ <div className='divider-light'/>
{emailSection}
- <div className="divider-light"/>
+ <div className='divider-light'/>
{pictureSection}
- <div className="divider-dark"/>
+ <div className='divider-dark'/>
</div>
</div>
);
}
});
-
var AppearanceTab = React.createClass({
submitTheme: function(e) {
e.preventDefault();
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
index 2b6d37f8d..bd25b7f84 100644
--- a/web/static/images/icons/audio.png
+++ b/web/static/images/icons/audio.png
Binary files differ
diff --git a/web/static/images/icons/code.png b/web/static/images/icons/code.png
index 80db302ee..c59e4b8dc 100644
--- a/web/static/images/icons/code.png
+++ b/web/static/images/icons/code.png
Binary files differ
diff --git a/web/static/images/icons/excel.png b/web/static/images/icons/excel.png
index 70ddadcbf..275c65c4d 100644
--- a/web/static/images/icons/excel.png
+++ b/web/static/images/icons/excel.png
Binary files differ
diff --git a/web/static/images/icons/generic.png b/web/static/images/icons/generic.png
index d9e82c232..0eb82c2d2 100644
--- a/web/static/images/icons/generic.png
+++ b/web/static/images/icons/generic.png
Binary files differ
diff --git a/web/static/images/icons/image.png b/web/static/images/icons/image.png
index a3acdef4c..799317731 100644
--- a/web/static/images/icons/image.png
+++ b/web/static/images/icons/image.png
Binary files differ
diff --git a/web/static/images/icons/patch.png b/web/static/images/icons/patch.png
index 18af126d4..a0affc9ee 100644
--- a/web/static/images/icons/patch.png
+++ b/web/static/images/icons/patch.png
Binary files differ
diff --git a/web/static/images/icons/pdf.png b/web/static/images/icons/pdf.png
index e4582570e..8c7507a1c 100644
--- a/web/static/images/icons/pdf.png
+++ b/web/static/images/icons/pdf.png
Binary files differ
diff --git a/web/static/images/icons/ppt.png b/web/static/images/icons/ppt.png
index 3571b4649..51553a11c 100644
--- a/web/static/images/icons/ppt.png
+++ b/web/static/images/icons/ppt.png
Binary files differ
diff --git a/web/static/images/icons/video.png b/web/static/images/icons/video.png
index e61a9e5f4..f53da93e4 100644
--- a/web/static/images/icons/video.png
+++ b/web/static/images/icons/video.png
Binary files differ
diff --git a/web/static/images/icons/word.png b/web/static/images/icons/word.png
index 20f830665..658937817 100644
--- a/web/static/images/icons/word.png
+++ b/web/static/images/icons/word.png
Binary files differ
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)
}