summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md5
-rw-r--r--api/user.go2
-rw-r--r--model/user.go1
-rw-r--r--store/sql_user_store.go8
-rw-r--r--store/store.go2
-rw-r--r--web/react/components/channel_header.jsx11
-rw-r--r--web/react/components/edit_channel_modal.jsx2
-rw-r--r--web/react/components/get_link_modal.jsx2
-rw-r--r--web/react/components/member_list_item.jsx2
-rw-r--r--web/react/components/member_list_team.jsx2
-rw-r--r--web/react/components/mention.jsx10
-rw-r--r--web/react/components/mention_list.jsx100
-rw-r--r--web/react/components/new_channel.jsx2
-rw-r--r--web/react/components/post_right.jsx2
-rw-r--r--web/react/components/sidebar_header.jsx2
-rw-r--r--web/react/components/user_profile.jsx4
-rw-r--r--web/react/components/user_settings.jsx21
-rw-r--r--web/react/utils/utils.jsx31
-rw-r--r--web/sass-files/sass/partials/_base.scss8
-rw-r--r--web/sass-files/sass/partials/_files.scss1
-rw-r--r--web/sass-files/sass/partials/_headers.scss26
-rw-r--r--web/sass-files/sass/partials/_mentions.scss4
-rw-r--r--web/sass-files/sass/partials/_modal.scss1
-rw-r--r--web/sass-files/sass/partials/_popover.scss9
-rw-r--r--web/sass-files/sass/partials/_post.scss2
-rw-r--r--web/sass-files/sass/partials/_responsive.scss4
-rw-r--r--web/sass-files/sass/partials/_settings.scss4
-rw-r--r--web/sass-files/sass/partials/_sidebar--right.scss8
-rw-r--r--web/sass-files/sass/styles.scss3
-rw-r--r--web/static/config/config.js4
-rw-r--r--web/static/images/dropdown-icon.pngbin2877 -> 0 bytes
31 files changed, 223 insertions, 60 deletions
diff --git a/README.md b/README.md
index 04cbf718d..a4f95dbdf 100644
--- a/README.md
+++ b/README.md
@@ -137,8 +137,5 @@ To contribute to this open source project please review the Mattermost Contribut
License
-------
-Most Mattermost source files are made available under the terms of the GNU Affero General Public License (AGPL). See individual files for details.
-
-As an exception, Admin Tools and Configuration Files are are made available under the terms of the Apache License, version 2.0. See LICENSE.txt for more information.
-
+Mattermost is licensed under an "Apache-wrapped AGPL" model, which means you can run and link to the system using Configuration Files and Admin Tools licensed under Apache, version 2.0, as described in the LICENSE file, as an explicit exception to the terms of the GNU Affero General Public License (AGPL) that applies to most of the remaining source files. See individual files for details.
diff --git a/api/user.go b/api/user.go
index 0c63868b3..3c0062f8c 100644
--- a/api/user.go
+++ b/api/user.go
@@ -729,7 +729,7 @@ func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- Srv.Store.User().UpdateUpdateAt(c.Session.UserId)
+ Srv.Store.User().UpdateLastPictureUpdate(c.Session.UserId)
c.LogAudit("")
}
diff --git a/model/user.go b/model/user.go
index b94ceb899..c5a4d846d 100644
--- a/model/user.go
+++ b/model/user.go
@@ -45,6 +45,7 @@ type User struct {
Props StringMap `json:"props"`
NotifyProps StringMap `json:"notify_props"`
LastPasswordUpdate int64 `json:"last_password_update"`
+ LastPictureUpdate int64 `json:"last_picture_update"`
}
// IsValid validates the user and returns an error if it isn't configured
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index 77470946c..665e4d697 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -37,6 +37,7 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore {
}
func (s SqlUserStore) UpgradeSchemaIfNeeded() {
+ s.CreateColumnIfNotExists("Users","LastPictureUpdate", "LastPasswordUpdate", "bigint(20)", "0")
}
func (us SqlUserStore) CreateIndexesIfNotExists() {
@@ -120,6 +121,7 @@ func (us SqlUserStore) Update(user *model.User, allowActiveUpdate bool) StoreCha
user.AuthData = oldUser.AuthData
user.Password = oldUser.Password
user.LastPasswordUpdate = oldUser.LastPasswordUpdate
+ user.LastPictureUpdate = oldUser.LastPictureUpdate
user.TeamId = oldUser.TeamId
user.LastActivityAt = oldUser.LastActivityAt
user.LastPingAt = oldUser.LastPingAt
@@ -150,13 +152,15 @@ func (us SqlUserStore) Update(user *model.User, allowActiveUpdate bool) StoreCha
return storeChannel
}
-func (us SqlUserStore) UpdateUpdateAt(userId string) StoreChannel {
+func (us SqlUserStore) UpdateLastPictureUpdate(userId string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
- if _, err := us.GetMaster().Exec("UPDATE Users SET UpdateAt = ? WHERE Id = ?", model.GetMillis(), userId); err != nil {
+ curTime := model.GetMillis()
+
+ if _, err := us.GetMaster().Exec("UPDATE Users SET LastPictureUpdate = ?, UpdateAt = ? WHERE Id = ?", curTime, curTime, userId); err != nil {
result.Err = model.NewAppError("SqlUserStore.UpdateUpdateAt", "We couldn't update the update_at", "user_id="+userId)
} else {
result.Data = userId
diff --git a/store/store.go b/store/store.go
index 0ed045788..9faa6a9d7 100644
--- a/store/store.go
+++ b/store/store.go
@@ -77,7 +77,7 @@ type PostStore interface {
type UserStore interface {
Save(user *model.User) StoreChannel
Update(user *model.User, allowRoleUpdate bool) StoreChannel
- UpdateUpdateAt(userId string) StoreChannel
+ UpdateLastPictureUpdate(userId string) StoreChannel
UpdateLastPingAt(userId string, time int64) StoreChannel
UpdateLastActivityAt(userId string, time int64) StoreChannel
UpdateUserAndSessionActivity(userId string, sessionId string, time int64) StoreChannel
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx
index 6791233d5..2e430489f 100644
--- a/web/react/components/channel_header.jsx
+++ b/web/react/components/channel_header.jsx
@@ -53,12 +53,12 @@ var PopoverListMembers = React.createClass({
});
members.forEach(function(m) {
- popoverHtml += "<div style='white-space: nowrap'>" + m.username + "</div>";
+ popoverHtml += "<div class='text--nowrap'>" + m.username + "</div>";
});
}
return (
- <div style={{cursor : "pointer"}} id="member_popover" data-toggle="popover" data-content={popoverHtml} data-original-title="Members" >
+ <div id="member_popover" data-toggle="popover" data-content={popoverHtml} data-original-title="Members" >
<div id="member_tooltip" data-toggle="tooltip" title="View Channel Members">
{count} <span className="glyphicon glyphicon-user" aria-hidden="true"></span>
</div>
@@ -203,11 +203,10 @@ module.exports = React.createClass({
<th><PopoverListMembers members={this.state.users} channelId={channel.id} /></th>
<th className="search-bar__container"><NavbarSearchBox /></th>
<th>
- <div className="dropdown" style={{marginLeft:5, marginRight:10}}>
+ <div className="dropdown channel-header__links">
<a href="#" className="dropdown-toggle theme" type="button" id="channel_header_right_dropdown" data-toggle="dropdown" aria-expanded="true">
- <i className="fa fa-caret-down"></i>
- </a>
- <ul className="dropdown-menu" role="menu" aria-labelledby="channel_header_right_dropdown" style={{left: "-150px"}}>
+ <span dangerouslySetInnerHTML={{__html: " <svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='4px' height='16px' viewBox='0 0 8 32' enable-background='new 0 0 8 32' xml:space='preserve'> <g> <circle cx='4' cy='4.062' r='4'/> <circle cx='4' cy='16' r='4'/> <circle cx='4' cy='28' r='4'/> </g> </svg>"}} /> </a>
+ <ul className="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="channel_header_right_dropdown">
<li role="presentation"><a role="menuitem" href="#" onClick={this.searchMentions}>Recent Mentions</a></li>
</ul>
</div>
diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx
index 255654fd5..c0818959a 100644
--- a/web/react/components/edit_channel_modal.jsx
+++ b/web/react/components/edit_channel_modal.jsx
@@ -43,7 +43,7 @@ module.exports = React.createClass({
<h4 className="modal-title" ref="title">Edit {this.state.title} Description</h4>
</div>
<div className="modal-body">
- <textarea className="form-control" style={{resize: "none"}} rows="6" ref="channelDesc" maxLength="1024" value={this.state.description} onChange={this.handleUserInput}></textarea>
+ <textarea className="form-control no-resize" rows="6" ref="channelDesc" maxLength="1024" value={this.state.description} onChange={this.handleUserInput}></textarea>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx
index bbfdce63a..af5314e64 100644
--- a/web/react/components/get_link_modal.jsx
+++ b/web/react/components/get_link_modal.jsx
@@ -38,7 +38,7 @@ module.exports = React.createClass({
<div className="modal-body">
<p>{"The link below is used for open " + strings.TeamPlural + " or if you allowed your " + strings.Team + " members to sign up using their " + strings.Company + " email addresses."}
</p>
- <textarea className="form-control" style={{resize: "none"}} readOnly="true" value={this.state.value}></textarea>
+ <textarea className="form-control no-resize" readOnly="true" value={this.state.value}></textarea>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-default" data-dismiss="modal">Close</button>
diff --git a/web/react/components/member_list_item.jsx b/web/react/components/member_list_item.jsx
index cf8c71d7e..a5279909b 100644
--- a/web/react/components/member_list_item.jsx
+++ b/web/react/components/member_list_item.jsx
@@ -49,7 +49,7 @@ module.exports = React.createClass({
</div>
);
} else {
- invite = <div className="member-role text-capitalize" style={{marginRight: 15}}>{member.roles || 'Member'}</div>;
+ invite = <div className="member-role text-capitalize">{member.roles || 'Member'}<span className="caret hidden"></span></div>;
}
return (
diff --git a/web/react/components/member_list_team.jsx b/web/react/components/member_list_team.jsx
index aa53c5db6..89b5e49d5 100644
--- a/web/react/components/member_list_team.jsx
+++ b/web/react/components/member_list_team.jsx
@@ -59,7 +59,7 @@ var MemberListTeamItem = React.createClass({
return {};
},
render: function() {
- var server_error = this.state.server_error ? <div style={{ clear: "both" }} className="has-error"><label className='has-error control-label'>{this.state.server_error}</label></div> : null;
+ var server_error = this.state.server_error ? <div className="has-error"><label className='has-error control-label'>{this.state.server_error}</label></div> : null;
var user = this.props.user;
var currentRoles = "Member";
var timestamp = UserStore.getCurrentUser().update_at;
diff --git a/web/react/components/mention.jsx b/web/react/components/mention.jsx
index 520b81cbb..114dc183f 100644
--- a/web/react/components/mention.jsx
+++ b/web/react/components/mention.jsx
@@ -6,16 +6,22 @@ module.exports = React.createClass({
handleClick: function() {
this.props.handleClick(this.props.username);
},
+ getInitialState: function() {
+ return null;
+ },
render: function() {
+ var self = this;
var icon;
var timestamp = UserStore.getCurrentUser().update_at;
- if (this.props.id != null) {
+ if (this.props.id === "allmention" || this.props.id === "channelmention") {
+ icon = <span><i className="mention-img fa fa-users fa-2x"></i></span>;
+ } else if (this.props.id != null) {
icon = <span><img className="mention-img" src={"/api/v1/users/" + this.props.id + "/image?time=" + timestamp}/></span>;
} else {
icon = <span><i className="mention-img fa fa-users fa-2x"></i></span>;
}
return (
- <div className="mentions-name" onClick={this.handleClick}>
+ <div className={"mentions-name " + this.props.isFocused} id={this.props.id + "_mentions"} onClick={this.handleClick} onMouseEnter={this.props.handleMouseEnter}>
<div className="pull-left">{icon}</div>
<div className="pull-left mention-align"><span>@{this.props.username}</span><span className="mention-fullname">{this.props.secondary_text}</span></div>
</div>
diff --git a/web/react/components/mention_list.jsx b/web/react/components/mention_list.jsx
index 103ff29bb..c5ff82346 100644
--- a/web/react/components/mention_list.jsx
+++ b/web/react/components/mention_list.jsx
@@ -17,14 +17,37 @@ module.exports = React.createClass({
displayName: "MentionList",
componentDidMount: function() {
PostStore.addMentionDataChangeListener(this._onChange);
-
var self = this;
- $('body').on('keypress.mentionlist', '#'+this.props.id,
+
+ $('body').on('keydown.mentionlist', '#'+this.props.id,
function(e) {
- if (!self.isEmpty() && self.state.mentionText != '-1' && e.which === 13) {
+ if (!self.isEmpty() && self.state.mentionText != '-1' && (e.which === 13 || e.which === 9)) {
+ e.stopPropagation();
+ e.preventDefault();
+ self.addCurrentMention();
+ }
+ else if (!self.isEmpty() && self.state.mentionText != '-1' && (e.which === 38 || e.which === 40)) {
e.stopPropagation();
e.preventDefault();
- self.addFirstMention();
+
+ var tempSelectedMention = -1;
+ if (e.which === 38) {
+ if (self.getSelection(self.state.selectedMention - 1))
+ self.setState({ selectedMention: self.state.selectedMention - 1, selectedUsername: self.refs['mention' + (self.state.selectedMention - 1)].props.username });
+ else {
+ while (self.getSelection(++tempSelectedMention))
+ ; //Need to find the top of the list
+ self.setState({ selectedMention: tempSelectedMention - 1, selectedUsername: self.refs['mention' + (tempSelectedMention - 1)].props.username });
+ }
+ }
+ else if (e.which === 40) {
+ if (self.getSelection(self.state.selectedMention + 1))
+ self.setState({ selectedMention: self.state.selectedMention + 1, selectedUsername: self.refs['mention' + (self.state.selectedMention + 1)].props.username });
+ else
+ self.setState({ selectedMention: 0, selectedUsername: self.refs.mention0.props.username });
+ }
+
+ self.scrollToMention(e.which, tempSelectedMention);
}
}
);
@@ -37,7 +60,28 @@ module.exports = React.createClass({
},
componentWillUnmount: function() {
PostStore.removeMentionDataChangeListener(this._onChange);
- $('body').off('keypress.mentionlist', '#'+this.props.id);
+ $('body').off('keydown.mentionlist', '#'+this.props.id);
+ },
+ componentDidUpdate: function() {
+ if (this.state.mentionText != "-1") {
+ if (this.state.selectedUsername !== "" && (!this.getSelection(this.state.selectedMention) || this.state.selectedUsername !== this.refs['mention' + this.state.selectedMention].props.username)) {
+ var tempSelectedMention = -1;
+ var foundMatch = false;
+ while (tempSelectedMention < this.state.selectedMention && this.getSelection(++tempSelectedMention)) {
+ if (this.state.selectedUsername === this.refs['mention' + tempSelectedMention].props.username) {
+ this.setState({ selectedMention: tempSelectedMention });
+ foundMatch = true;
+ break;
+ }
+ }
+ if (this.getSelection(0) && !foundMatch) {
+ this.setState({ selectedMention: 0, selectedUsername: this.refs.mention0.props.username });
+ }
+ }
+ }
+ else if (this.state.selectedMention !== 0) {
+ this.setState({ selectedMention: 0, selectedUsername: "" });
+ }
},
_onChange: function(id, mentionText, excludeList) {
if (id !== this.props.id) return;
@@ -45,6 +89,7 @@ module.exports = React.createClass({
var newState = this.state;
if (mentionText != null) newState.mentionText = mentionText;
if (excludeList != null) newState.excludeUsers = excludeList;
+
this.setState(newState);
},
handleClick: function(name) {
@@ -56,6 +101,21 @@ module.exports = React.createClass({
this.setState({ mentionText: '-1' });
},
+ handleMouseEnter: function(listId) {
+ this.setState({ selectedMention: listId, selectedUsername: this.refs['mention' + listId].props.username });
+ },
+ getSelection: function(listId) {
+ if (!this.refs['mention' + listId])
+ return false;
+ else
+ return true;
+ },
+ addCurrentMention: function() {
+ if (!this.getSelection(this.state.selectedMention))
+ this.addFirstMention();
+ else
+ this.refs['mention' + this.state.selectedMention].handleClick();
+ },
addFirstMention: function() {
if (!this.refs.mention0) return;
this.refs.mention0.handleClick();
@@ -63,6 +123,23 @@ module.exports = React.createClass({
isEmpty: function() {
return (!this.refs.mention0);
},
+ scrollToMention: function(keyPressed, ifLoopUp) {
+ var direction = keyPressed === 38 ? "up" : "down";
+ var scrollAmount = 0;
+
+ if (direction === "up" && ifLoopUp !== -1)
+ scrollAmount = $("#mentionsbox").height() * 100; //Makes sure that it scrolls all the way to the bottom
+ else if (direction === "down" && this.state.selectedMention === 0)
+ scrollAmount = 0;
+ else if (direction === "up")
+ scrollAmount = "-=" + ($('#'+this.refs['mention' + this.state.selectedMention].props.id +"_mentions").innerHeight() - 5);
+ else if (direction === "down")
+ scrollAmount = "+=" + ($('#'+this.refs['mention' + this.state.selectedMention].props.id +"_mentions").innerHeight() - 5);
+
+ $("#mentionsbox").animate({
+ scrollTop: scrollAmount
+ }, 75);
+ },
alreadyMentioned: function(username) {
var excludeUsers = this.state.excludeUsers;
for (var i = 0; i < excludeUsers.length; i++) {
@@ -73,9 +150,10 @@ module.exports = React.createClass({
return false;
},
getInitialState: function() {
- return { excludeUsers: [], mentionText: "-1" };
+ return { excludeUsers: [], mentionText: "-1", selectedMention: 0, selectedUsername: "" };
},
render: function() {
+ var self = this;
var mentionText = this.state.mentionText;
if (mentionText === '-1') return null;
@@ -89,12 +167,14 @@ module.exports = React.createClass({
all.username = "all";
all.full_name = "";
all.secondary_text = "Notifies everyone in the team";
+ all.id = "allmention";
users.push(all);
var channel = {};
channel.username = "channel";
channel.full_name = "";
channel.secondary_text = "Notifies everyone in the channel";
+ channel.id = "channelmention";
users.push(channel);
users.sort(function(a,b) {
@@ -118,17 +198,21 @@ module.exports = React.createClass({
if (firstName.lastIndexOf(mentionText,0) === 0
|| lastName.lastIndexOf(mentionText,0) === 0 || users[i].username.lastIndexOf(mentionText,0) === 0) {
- mentions[i+1] = (
+ mentions[index] = (
<Mention
ref={'mention' + index}
username={users[i].username}
secondary_text={users[i].secondary_text}
id={users[i].id}
+ listId={index}
+ isFocused={this.state.selectedMention === index ? "mentions-focus" : ""}
+ handleMouseEnter={function(value) { return function() { self.handleMouseEnter(value); } }(index)}
handleClick={this.handleClick} />
);
index++;
}
}
+
var numMentions = Object.keys(mentions).length;
if (numMentions < 1) return null;
@@ -144,7 +228,7 @@ module.exports = React.createClass({
return (
<div className="mentions--top" style={style}>
- <div ref="mentionlist" className="mentions-box">
+ <div ref="mentionlist" className="mentions-box" id="mentionsbox">
{ mentions }
</div>
</div>
diff --git a/web/react/components/new_channel.jsx b/web/react/components/new_channel.jsx
index 160241c1c..069e0d6b1 100644
--- a/web/react/components/new_channel.jsx
+++ b/web/react/components/new_channel.jsx
@@ -122,7 +122,7 @@ module.exports = React.createClass({
</div>
<div className="form-group">
<label className='control-label'>Description</label>
- <textarea className="form-control" style={{resize: "none"}} ref="channel_desc" rows="3" placeholder="Description" maxLength="1024"></textarea>
+ <textarea className="form-control no-resize" ref="channel_desc" rows="3" placeholder="Description" maxLength="1024"></textarea>
</div>
{ server_error }
</form>
diff --git a/web/react/components/post_right.jsx b/web/react/components/post_right.jsx
index 408fbf83a..dca05051d 100644
--- a/web/react/components/post_right.jsx
+++ b/web/react/components/post_right.jsx
@@ -43,7 +43,7 @@ RhsHeaderPost = React.createClass({
});
},
render: function() {
- var back = this.props.fromSearch ? <a href="#" onClick={this.handleBack} style={{color:"black"}}>{"< "}</a> : "";
+ var back = this.props.fromSearch ? <a href="#" onClick={this.handleBack} className="sidebar--right__back"><i className="fa fa-chevron-left"></i></a> : "";
return (
<div className="sidebar--right__header">
diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx
index 334fc6780..45c9ca629 100644
--- a/web/react/components/sidebar_header.jsx
+++ b/web/react/components/sidebar_header.jsx
@@ -79,7 +79,7 @@ var NavbarDropdown = React.createClass({
<ul className="nav navbar-nav navbar-right">
<li className="dropdown">
<a href="#" className="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
- <i className="dropdown__icon"></i>
+ <span className="dropdown__icon" dangerouslySetInnerHTML={{__html: " <svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='4px' height='16px' viewBox='0 0 8 32' enable-background='new 0 0 8 32' xml:space='preserve'> <g> <circle cx='4' cy='4.062' r='4'/> <circle cx='4' cy='16' r='4'/> <circle cx='4' cy='28' r='4'/> </g> </svg>"}} />
</a>
<ul className="dropdown-menu" role="menu">
<li><a href="#" data-toggle="modal" data-target="#user_settings1">Account Settings</a></li>
diff --git a/web/react/components/user_profile.jsx b/web/react/components/user_profile.jsx
index 89d0a80ff..65f025919 100644
--- a/web/react/components/user_profile.jsx
+++ b/web/react/components/user_profile.jsx
@@ -53,7 +53,7 @@ module.exports = React.createClass({
var name = this.props.overwriteName ? this.props.overwriteName : this.state.profile.username;
- var data_content = "<img style='margin: 10px' src='/api/v1/users/" + this.state.profile.id + "/image?time=" + this.state.profile.update_at + "' height='128' width='128' />";
+ var data_content = "<img class='user-popover__image' src='/api/v1/users/" + this.state.profile.id + "/image?time=" + this.state.profile.update_at + "' height='128' width='128' />";
if (!config.ShowEmail) {
data_content += "<div class='text-nowrap'>Email not shared</div>";
} else {
@@ -61,7 +61,7 @@ module.exports = React.createClass({
}
return (
- <div style={{"cursor" : "pointer", "display" : "inline-block"}} className="user-popover" id={"profile_" + this.uniqueId} data-toggle="popover" data-content={data_content} data-original-title={this.state.profile.username} >
+ <div className="user-popover" id={"profile_" + this.uniqueId} data-toggle="popover" data-content={data_content} data-original-title={this.state.profile.username} >
{ name }
</div>
);
diff --git a/web/react/components/user_settings.jsx b/web/react/components/user_settings.jsx
index 06d8d0208..38e4b1aea 100644
--- a/web/react/components/user_settings.jsx
+++ b/web/react/components/user_settings.jsx
@@ -560,7 +560,7 @@ var AuditTab = React.createClass({
<div className="user-settings">
<h3 className="tab-header">Activity Log</h3>
<div className="divider-dark first"/>
- <div className="table-responsive" style={{ maxWidth: "560px", maxHeight: "300px" }}>
+ <div className="table-responsive">
<table className="table-condensed small">
<thead>
<tr>
@@ -576,11 +576,11 @@ var AuditTab = React.createClass({
this.state.audits.map(function(value, index) {
return (
<tr key={ "" + index }>
- <td style={{ whiteSpace: "nowrap" }}>{ new Date(value.create_at).toLocaleString() }</td>
- <td style={{ whiteSpace: "nowrap" }}>{ value.action.replace("/api/v1", "") }</td>
- <td style={{ whiteSpace: "nowrap" }}>{ value.ip_address }</td>
- <td style={{ whiteSpace: "nowrap" }}>{ value.session_id }</td>
- <td style={{ whiteSpace: "nowrap" }}>{ value.extra_info }</td>
+ <td className="text-nowrap">{ new Date(value.create_at).toLocaleString() }</td>
+ <td className="text-nowrap">{ value.action.replace("/api/v1", "") }</td>
+ <td className="text-nowrap">{ value.ip_address }</td>
+ <td className="text-nowrap">{ value.session_id }</td>
+ <td className="text-nowrap">{ value.extra_info }</td>
</tr>
);
}, this)
@@ -820,6 +820,7 @@ var GeneralTab = React.createClass({
client.uploadProfileImage(formData,
function(data) {
this.submitActive = false;
+ AsyncClient.getMe();
window.location.reload();
}.bind(this),
function(err) {
@@ -989,7 +990,7 @@ var GeneralTab = React.createClass({
<SettingPicture
title="Profile Picture"
submit={this.submitPicture}
- src={"/api/v1/users/" + user.id + "/image"}
+ 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();}}
@@ -1000,10 +1001,14 @@ var GeneralTab = React.createClass({
);
} else {
+ var minMessage = "Click Edit to upload an image.";
+ if (user.last_picture_update) {
+ minMessage = "Image last updated " + utils.displayDate(user.last_picture_update)
+ }
pictureSection = (
<SettingItemMin
title="Profile Picture"
- describe="Picture inside."
+ describe={minMessage}
updateSection={function(){self.updateSection("picture");}}
/>
);
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 19c074606..7186251e7 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -198,7 +198,13 @@ module.exports.getTimestamp = function() {
}
var testUrlMatch = function(text) {
- var urlMatcher = new Autolinker.matchParser.MatchParser;
+ var urlMatcher = new Autolinker.matchParser.MatchParser({
+ urls: true,
+ emails: false,
+ twitter: false,
+ phone: false,
+ hashtag: false,
+ });
var result = [];
var replaceFn = function(match) {
var linkData = {};
@@ -303,18 +309,25 @@ var getYoutubeEmbed = function(link) {
};
var success = function(data) {
- $('.video-uploader.'+youtubeId).html(data.data.uploader);
- $('.video-title.'+youtubeId).find('a').html(data.data.title);
+ if(!data.items.length || !data.items[0].snippet) {
+ return;
+ }
+ var metadata = data.items[0].snippet;
+ $('.video-uploader.'+youtubeId).html(metadata.channelTitle);
+ $('.video-title.'+youtubeId).find('a').html(metadata.title);
$(".post-list-holder-by-time").scrollTop($(".post-list-holder-by-time")[0].scrollHeight);
$(".post-list-holder-by-time").perfectScrollbar('update');
};
- $.ajax({
- async: true,
- url: 'https://gdata.youtube.com/feeds/api/videos/'+youtubeId+'?v=2&alt=jsonc',
- type: 'GET',
- success: success
- });
+ if(config.GoogleDeveloperKey) {
+ $.ajax({
+ async: true,
+ url: "https://www.googleapis.com/youtube/v3/videos",
+ type: 'GET',
+ data: {part:"snippet", id:youtubeId, key:config.GoogleDeveloperKey},
+ success: success
+ });
+ }
return (
<div className="post-comment">
diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss
index 8f4ff7b60..1fb970075 100644
--- a/web/sass-files/sass/partials/_base.scss
+++ b/web/sass-files/sass/partials/_base.scss
@@ -10,6 +10,9 @@ body {
height: 100%;
&.white {
background: #fff;
+ > .container-fluid {
+ overflow: auto;
+ }
.inner__wrap {
> .row.content {
min-height: 100%;
@@ -53,6 +56,9 @@ div.theme {
.form-control {
@include border-radius(2px);
+ &.no-resize {
+ resize: none;
+ }
}
.form-group {
@@ -127,7 +133,7 @@ div.theme {
}
.glyphicon-refresh-animate {
- @include animation(spin .7s infinite linear);
+ @include animation(spin .7s infinite linear);
}
.black-bg {
diff --git a/web/sass-files/sass/partials/_files.scss b/web/sass-files/sass/partials/_files.scss
index c584c240d..56d03e171 100644
--- a/web/sass-files/sass/partials/_files.scss
+++ b/web/sass-files/sass/partials/_files.scss
@@ -32,6 +32,7 @@
}
}
.preview-img {
+ display: block;
height: auto;
max-width: 100%;
}
diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss
index 338f5ceb4..d876d8b37 100644
--- a/web/sass-files/sass/partials/_headers.scss
+++ b/web/sass-files/sass/partials/_headers.scss
@@ -95,11 +95,7 @@
}
}
.dropdown__icon {
- background: url("../images/dropdown-icon.png");
- width: 4px;
- height: 16px;
- @include background-size(100% 100%);
- display: inline-block;
+ fill: #fff;
}
}
.user__picture {
@@ -163,7 +159,7 @@
#member_popover {
width: 45px;
color: #999;
-
+ cursor: pointer;
}
&.alt {
margin: 0;
@@ -230,3 +226,21 @@
top: 1px;
}
}
+
+.channel-header__links {
+ height: 32px;
+ vertical-align: top;
+ display: inline-block;
+ width: 15px;
+ margin: 9px 3px 3px 0;
+ &:hover {
+ svg {
+ fill: #888;
+ }
+ }
+ svg {
+ vertical-align: top;
+ margin-top: 8px;
+ fill: #AAA;
+ }
+} \ No newline at end of file
diff --git a/web/sass-files/sass/partials/_mentions.scss b/web/sass-files/sass/partials/_mentions.scss
index 7e8c1869a..1396f21a1 100644
--- a/web/sass-files/sass/partials/_mentions.scss
+++ b/web/sass-files/sass/partials/_mentions.scss
@@ -37,6 +37,10 @@
}
}
+.mentions-focus {
+ background-color: #E6F2FA;
+}
+
.mentions-text {
font-color:black;
}
diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss
index 707e71cf0..1b0338884 100644
--- a/web/sass-files/sass/partials/_modal.scss
+++ b/web/sass-files/sass/partials/_modal.scss
@@ -154,6 +154,7 @@
background: #FFF;
position: relative;
max-width: 90%;
+ min-height: 50px;
min-width: 280px;
@include border-radius(3px);
display: table;
diff --git a/web/sass-files/sass/partials/_popover.scss b/web/sass-files/sass/partials/_popover.scss
new file mode 100644
index 000000000..fa1b44841
--- /dev/null
+++ b/web/sass-files/sass/partials/_popover.scss
@@ -0,0 +1,9 @@
+.user-popover {
+ cursor: pointer;
+ display: inline-block;
+}
+
+.user-popover__image {
+ margin: 0 0 10px;
+ @include border-radius(128px);
+} \ No newline at end of file
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index 40ed40b49..465c50296 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -425,4 +425,4 @@ body.ios {
width: 40px;
}
}
-}
+} \ No newline at end of file
diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss
index e01739240..1f9643175 100644
--- a/web/sass-files/sass/partials/_responsive.scss
+++ b/web/sass-files/sass/partials/_responsive.scss
@@ -413,7 +413,7 @@
}
}
.footer, .footer-pane, .footer-push {
- height: 185px;
+ height: auto;
}
.footer-pane {
.footer-link {
@@ -574,6 +574,8 @@
.modal {
.modal-image {
.image-wrapper {
+ font-size: 12px;
+ max-width: 280px;
.modal-close {
@include opacity(1);
}
diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss
index af759c650..e60bc290e 100644
--- a/web/sass-files/sass/partials/_settings.scss
+++ b/web/sass-files/sass/partials/_settings.scss
@@ -1,6 +1,10 @@
.user-settings {
background: #fff;
min-height:300px;
+ .table-responsive {
+ max-width: 560px;
+ max-height: 300px;
+ }
}
.settings-modal {
diff --git a/web/sass-files/sass/partials/_sidebar--right.scss b/web/sass-files/sass/partials/_sidebar--right.scss
index a0e82fd2f..d02a92448 100644
--- a/web/sass-files/sass/partials/_sidebar--right.scss
+++ b/web/sass-files/sass/partials/_sidebar--right.scss
@@ -10,6 +10,14 @@
&.move--left {
right: 0;
}
+ .sidebar--right__back {
+ color: #666;
+ width: 20px;
+ text-align: center;
+ margin: 0 0 0 -6px;
+ font-size: 12px;
+ display: inline-block;
+ }
.sidebar-right__body {
border-left: $border-gray;
border-top: $border-gray;
diff --git a/web/sass-files/sass/styles.scss b/web/sass-files/sass/styles.scss
index 9cc26320c..294f6122a 100644
--- a/web/sass-files/sass/styles.scss
+++ b/web/sass-files/sass/styles.scss
@@ -15,6 +15,7 @@
@import "partials/headers";
@import "partials/footer";
@import "partials/content";
+@import "partials/popover";
@import "partials/post";
@import "partials/post_right";
@import "partials/navbar";
@@ -29,7 +30,7 @@
@import "partials/modal";
@import "partials/mentions";
@import "partials/error";
-@import "partials/loading";
+@import "partials/loading";
// Responsive Css
@import "partials/responsive";
diff --git a/web/static/config/config.js b/web/static/config/config.js
index 45c713da2..0d564b77e 100644
--- a/web/static/config/config.js
+++ b/web/static/config/config.js
@@ -16,6 +16,10 @@ var config = {
RequireInviteNames: false,
AllowSignupDomainsWizard: false,
+ // Google Developer Key (for Youtube API links)
+ // Leave blank to disable
+ GoogleDeveloperKey: "",
+
// Privacy switches
ShowEmail: true,
diff --git a/web/static/images/dropdown-icon.png b/web/static/images/dropdown-icon.png
deleted file mode 100644
index 5c271cfc7..000000000
--- a/web/static/images/dropdown-icon.png
+++ /dev/null
Binary files differ