diff options
Diffstat (limited to 'web')
41 files changed, 629 insertions, 440 deletions
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 68de80228..30435dc08 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -1,6 +1,7 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. + var ChannelStore = require('../stores/channel_store.jsx'); var UserStore = require('../stores/user_store.jsx'); var PostStore = require('../stores/post_store.jsx'); @@ -16,7 +17,7 @@ var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; -var ExtraMembers = React.createClass({ +var PopoverListMembers = React.createClass({ componentDidMount: function() { var originalLeave = $.fn.popover.Constructor.prototype.leave; $.fn.popover.Constructor.prototype.leave = function(obj) { @@ -35,30 +36,29 @@ var ExtraMembers = React.createClass({ $("#member_popover").popover({placement : 'bottom', trigger: 'click', html: true}); $('body').on('click', function (e) { - if ($(e.target.parentNode.parentNode)[0] !== $("#member_popover")[0] && $(e.target).parents('.popover.in').length === 0) { + if ($(e.target.parentNode.parentNode)[0] !== $("#member_popover")[0] && $(e.target).parents('.popover.in').length === 0) { $("#member_popover").popover('hide'); } }); - }, + render: function() { - var count = this.props.members.length == 0 ? "-" : this.props.members.length; - count = this.props.members.length > 19 ? "20+" : count; - var data_content = ""; - var sortedMembers = this.props.members; + var popoverHtml = ''; + var members = this.props.members; + var count = (members.length > 20) ? "20+" : (members.length || '-'); - if(sortedMembers) { - sortedMembers.sort(function(a,b) { + if (members) { + members.sort(function(a,b) { return a.username.localeCompare(b.username); - }) + }); - sortedMembers.forEach(function(m) { - data_content += "<div style='white-space: nowrap'>" + m.username + "</div>"; + members.forEach(function(m) { + popoverHtml += "<div class='text--nowrap'>" + m.username + "</div>"; }); } return ( - <div style={{"cursor" : "pointer"}} id="member_popover" data-toggle="popover" data-content={data_content} 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> @@ -78,6 +78,7 @@ function getStateFromStores() { } module.exports = React.createClass({ + displayName: 'ChannelHeader', componentDidMount: function() { ChannelStore.addChangeListener(this._onChange); ChannelStore.addExtraInfoChangeListener(this._onChange); @@ -99,7 +100,7 @@ module.exports = React.createClass({ $(".channel-header__info .description").popover({placement : 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}}); }, _onSocketChange: function(msg) { - if(msg.action === "new_user") { + if (msg.action === "new_user") { AsyncClient.getChannelExtraInfo(true); } }, @@ -107,15 +108,14 @@ module.exports = React.createClass({ return getStateFromStores(); }, handleLeave: function(e) { - var self = this; Client.leaveChannel(this.state.channel.id, function(data) { var townsquare = ChannelStore.getByName('town-square'); utils.switchChannel(townsquare); - }.bind(this), + }, function(err) { AsyncClient.dispatchError(err, "handleLeave"); - }.bind(this) + } ); }, searchMentions: function(e) { @@ -131,52 +131,29 @@ module.exports = React.createClass({ AppDispatcher.handleServerAction({ type: ActionTypes.RECIEVED_SEARCH_TERM, term: terms, - do_search: false + do_search: true, + is_mention_search: true }); - - Client.search( - terms, - function(data) { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_SEARCH, - results: data, - is_mention_search: true - }); - }, - function(err) { - dispatchError(err, "search"); - } - ); }, + render: function() { if (this.state.channel == null) { - return ( - <div></div> - ); + return null; } - var description = utils.textToJsx(this.state.channel.description, {"singleline": true, "noMentionHighlight": true}); - var popoverContent = React.renderToString(<MessageWrapper message={this.state.channel.description}/>); - var channelTitle = ""; - var channelName = this.state.channel.name; + var channel = this.state.channel; + var description = utils.textToJsx(channel.description, {"singleline": true, "noMentionHighlight": true}); + var popoverContent = React.renderToString(<MessageWrapper message={channel.description}/>); + var channelTitle = channel.display_name; var currentId = UserStore.getCurrentId(); var isAdmin = this.state.memberChannel.roles.indexOf("admin") > -1 || this.state.memberTeam.roles.indexOf("admin") > -1; - var searchForm = <th className="search-bar__container"><NavbarSearchBox /></th>; - var isDirect = false; - - if (this.state.channel.type === 'O') { - channelTitle = this.state.channel.display_name; - } else if (this.state.channel.type === 'P') { - channelTitle = this.state.channel.display_name; - } else if (this.state.channel.type === 'D') { - isDirect = true; + var isDirect = (this.state.channel.type === 'D'); + + if (isDirect) { if (this.state.users.length > 1) { - if (this.state.users[0].id === UserStore.getCurrentId()) { - channelTitle = <UserProfile userId={this.state.users[1].id} overwriteName={this.state.users[1].full_name ? this.state.users[1].full_name : this.state.users[1].username} />; - } else { - channelTitle = <UserProfile userId={this.state.users[0].id} overwriteName={this.state.users[0].full_name ? this.state.users[0].full_name : this.state.users[0].username} />; - } + var contact = this.state.users[((this.state.users[0].id === currentId) ? 1 : 0)]; + channelTitle = <UserProfile userId={contact.id} overwriteName={contact.nickname || contact.username} />; } } @@ -192,25 +169,28 @@ module.exports = React.createClass({ <span className="glyphicon glyphicon-chevron-down header-dropdown__icon"></span> </a> <ul className="dropdown-menu" role="menu" aria-labelledby="channel_header_dropdown"> - <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_info" data-channelid={this.state.channel.id} href="#">View Info</a></li> - <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_invite" href="#">Add Members</a></li> - { isAdmin ? + <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_info" data-channelid={channel.id} href="#">View Info</a></li> + { !ChannelStore.isDefault(channel) ? + <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_invite" href="#">Add Members</a></li> + : null + } + { isAdmin && !ChannelStore.isDefault(channel) ? <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_members" href="#">Manage Members</a></li> - : "" + : null } - <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#edit_channel" data-desc={this.state.channel.description} data-title={this.state.channel.display_name} data-channelid={this.state.channel.id}>Set Channel Description...</a></li> - <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#channel_notifications" data-title={this.state.channel.display_name} data-channelid={this.state.channel.id}>Notification Preferences</a></li> - { isAdmin && channelName != Constants.DEFAULT_CHANNEL ? - <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#rename_channel" data-display={this.state.channel.display_name} data-name={this.state.channel.name} data-channelid={this.state.channel.id}>Rename Channel...</a></li> - : "" + <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#edit_channel" data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id}>Set Channel Description...</a></li> + <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#channel_notifications" data-title={channel.display_name} data-channelid={channel.id}>Notification Preferences</a></li> + { isAdmin && !ChannelStore.isDefault(channel) ? + <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#rename_channel" data-display={channel.display_name} data-name={channel.name} data-channelid={channel.id}>Rename Channel...</a></li> + : null } - { isAdmin && channelName != Constants.DEFAULT_CHANNEL ? - <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#delete_channel" data-title={this.state.channel.display_name} data-channelid={this.state.channel.id}>Delete Channel...</a></li> - : "" + { isAdmin && !ChannelStore.isDefault(channel) ? + <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#delete_channel" data-title={channel.display_name} data-channelid={channel.id}>Delete Channel...</a></li> + : null } - { channelName != Constants.DEFAULT_CHANNEL ? + { !ChannelStore.isDefault(channel) ? <li role="presentation"><a role="menuitem" href="#" onClick={this.handleLeave}>Leave Channel</a></li> - : "" + : null } </ul> </div> @@ -220,14 +200,13 @@ module.exports = React.createClass({ <a href="#"><strong className="heading">{channelTitle}</strong></a> } </th> - <th><ExtraMembers members={this.state.users} channelId={this.state.channel.id} /></th> - { searchForm } + <th><PopoverListMembers members={this.state.users} channelId={channel.id} /></th> + <th className="search-bar__container"><NavbarSearchBox /></th> <th> - <div className="dropdown" style={{"marginLeft":"5px", "marginRight":"10px"}}> + <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> @@ -237,5 +216,3 @@ module.exports = React.createClass({ ); } }); - - 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..6f1d83193 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; @@ -85,8 +85,8 @@ var MemberListTeamItem = React.createClass({ return ( <div className="row member-div"> <img className="post-profile-img pull-left" src={"/api/v1/users/" + user.id + "/image?time=" + timestamp} height="36" width="36" /> - <span className="member-name">{user.full_name.trim() ? user.full_name : user.username}</span> - <span className="member-email">{user.full_name.trim() ? user.username : email}</span> + <span className="member-name">{utils.getDisplayName(user)}</span> + <span className="member-email">{email}</span> <div className="dropdown member-drop"> <a href="#" className="dropdown-toggle theme" type="button" id="channel_header_dropdown" data-toggle="dropdown" aria-expanded="true"> <span>{currentRoles} </span> 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..524f1b337 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; @@ -87,14 +165,16 @@ module.exports = React.createClass({ var all = {}; all.username = "all"; - all.full_name = ""; + all.nickname = ""; all.secondary_text = "Notifies everyone in the team"; + all.id = "allmention"; users.push(all); var channel = {}; channel.username = "channel"; - channel.full_name = ""; + channel.nickname = ""; channel.secondary_text = "Notifies everyone in the channel"; + channel.id = "channelmention"; users.push(channel); users.sort(function(a,b) { @@ -108,27 +188,23 @@ module.exports = React.createClass({ for (var i = 0; i < users.length && index < MAX_ITEMS_IN_LIST; i++) { if (this.alreadyMentioned(users[i].username)) continue; - var firstName = "", lastName = ""; - if (users[i].full_name.length > 0) { - var splitName = users[i].full_name.split(' '); - firstName = splitName[0].toLowerCase(); - lastName = splitName.length > 1 ? splitName[splitName.length-1].toLowerCase() : ""; - users[i].secondary_text = users[i].full_name; - } - - if (firstName.lastIndexOf(mentionText,0) === 0 - || lastName.lastIndexOf(mentionText,0) === 0 || users[i].username.lastIndexOf(mentionText,0) === 0) { - mentions[i+1] = ( + if (users[i].first_name.lastIndexOf(mentionText,0) === 0 + || users[i].last_name.lastIndexOf(mentionText,0) === 0 || users[i].username.lastIndexOf(mentionText,0) === 0) { + mentions[index] = ( <Mention ref={'mention' + index} username={users[i].username} - secondary_text={users[i].secondary_text} + secondary_text={users[i].first_name + " " + users[i].last_name} 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 +220,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/navbar.jsx b/web/react/components/navbar.jsx index 35f7d9044..78cf7d8b8 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -2,21 +2,20 @@ // See License.txt for license information. -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); -var Sidebar = require('./sidebar.jsx'); var UserStore = require('../stores/user_store.jsx'); -var SocketStore = require('../stores/socket_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); -var Constants = require('../utils/constants.jsx'); + var UserProfile = require('./user_profile.jsx'); var MessageWrapper = require('./message_wrapper.jsx'); + +var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; +var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); function getCountsStateFromStores() { - var count = 0; var channels = ChannelStore.getAll(); var members = ChannelStore.getAllMembers(); @@ -34,7 +33,7 @@ function getCountsStateFromStores() { } }); - return { count: count } + return { count: count }; } var NotifyCounts = React.createClass({ @@ -54,11 +53,10 @@ var NotifyCounts = React.createClass({ return getCountsStateFromStores(); }, render: function() { - if (this.state.count == 0) { - return (<span></span>); - } - else { - return (<span className="badge badge-notify">{ this.state.count }</span>); + if (this.state.count) { + return <span className="badge badge-notify">{ this.state.count }</span>; + } else { + return null; } } }); @@ -66,25 +64,25 @@ var NotifyCounts = React.createClass({ var NavbarLoginForm = React.createClass({ handleSubmit: function(e) { e.preventDefault(); - var state = { } + var state = { }; var domain = this.refs.domain.getDOMNode().value.trim(); if (!domain) { - state.server_error = "A domain is required" + state.server_error = "A domain is required"; this.setState(state); return; } var email = this.refs.email.getDOMNode().value.trim(); if (!email) { - state.server_error = "An email is required" + state.server_error = "An email is required"; this.setState(state); return; } var password = this.refs.password.getDOMNode().value.trim(); if (!password) { - state.server_error = "A password is required" + state.server_error = "A password is required"; this.setState(state); return; } @@ -105,7 +103,7 @@ var NavbarLoginForm = React.createClass({ window.location.href = '/channels/town-square'; } - }.bind(this), + }, function(err) { if (err.message == "Login failed because email address has not been verified") { window.location.href = '/verify?domain=' + encodeURIComponent(domain) + '&email=' + encodeURIComponent(email); @@ -159,13 +157,14 @@ function getStateFromStores() { } module.exports = React.createClass({ + displayName: 'Navbar', + componentDidMount: function() { ChannelStore.addChangeListener(this._onChange); ChannelStore.addExtraInfoChangeListener(this._onChange); - var self = this; - $('.inner__wrap').click(self.hideSidebars); + $('.inner__wrap').click(this.hideSidebars); - $('body').on('click.infopopover', function(e){ + $('body').on('click.infopopover', function(e) { if ($(e.target).attr('data-toggle') !== 'popover' && $(e.target).parents('.popover.in').length === 0) { $('.info-popover').popover('hide'); @@ -181,13 +180,13 @@ module.exports = React.createClass({ }, handleLeave: function(e) { client.leaveChannel(this.state.channel.id, - function(data) { + function() { AsyncClient.getChannels(true); window.location.href = '/channels/town-square'; - }.bind(this), + }, function(err) { AsyncClient.dispatchError(err, "handleLeave"); - }.bind(this) + } ); }, hideSidebars: function(e) { @@ -204,7 +203,7 @@ module.exports = React.createClass({ }); if (e.target.className != 'navbar-toggle' && e.target.className != 'icon-bar') { - $('.inner__wrap').removeClass('move--right').removeClass('move--left').removeClass('move--left-small'); + $('.inner__wrap').removeClass('move--right move--left move--left-small'); $('.sidebar--left').removeClass('move--right'); $('.sidebar--right').removeClass('move--left'); $('.sidebar--menu').removeClass('move--left'); @@ -229,24 +228,22 @@ module.exports = React.createClass({ render: function() { var currentId = UserStore.getCurrentId(); - var channelName = ""; var popoverContent = ""; var channelTitle = this.props.teamName; var isAdmin = false; var isDirect = false; var description = "" + var channel = this.state.channel; - if (this.state.channel) { - var channel = this.state.channel; - description = utils.textToJsx(this.state.channel.description, {"singleline": true, "noMentionHighlight": true}); - popoverContent = React.renderToString(<MessageWrapper message={this.state.channel.description}/>); - channelName = this.state.channel.name; + if (channel) { + description = utils.textToJsx(channel.description, {"singleline": true, "noMentionHighlight": true}); + popoverContent = React.renderToString(<MessageWrapper message={channel.description}/>); isAdmin = this.state.member.roles.indexOf("admin") > -1; if (channel.type === 'O') { - channelTitle = this.state.channel.display_name; + channelTitle = channel.display_name; } else if (channel.type === 'P') { - channelTitle = this.state.channel.display_name; + channelTitle = channel.display_name; } else if (channel.type === 'D') { isDirect = true; if (this.state.users.length > 1) { @@ -258,12 +255,11 @@ module.exports = React.createClass({ } } - if(this.state.channel.description.length == 0){ - popoverContent = React.renderToString(<div>No channel description yet. <br /><a href='#' data-toggle='modal' data-desc={this.state.channel.description} data-title={this.state.channel.display_name} data-channelid={this.state.channel.id} data-target='#edit_channel'>Click here</a> to add one.</div>); + if (channel.description.length == 0) { + popoverContent = React.renderToString(<div>No channel description yet. <br /><a href='#' data-toggle='modal' data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id} data-target='#edit_channel'>Click here</a> to add one.</div>); } } - var loginForm = currentId == null ? <NavbarLoginForm /> : null; var navbar_collapse_button = currentId != null ? null : <button type="button" className="navbar-toggle" data-toggle="collapse" data-target="#navbar-collapse-1"> <span className="sr-only">Toggle sidebar</span> @@ -292,60 +288,61 @@ module.exports = React.createClass({ { navbar_collapse_button } { sidebar_collapse_button } { right_sidebar_collapse_button } - { !isDirect && this.state.channel ? - <div className="navbar-brand"> - <div className="dropdown"> - <div data-toggle="popover" data-content={popoverContent} className="description info-popover"></div> - <a href="#" className="dropdown-toggle theme" type="button" id="channel_header_dropdown" data-toggle="dropdown" aria-expanded="true"> - <strong className="heading">{channelTitle} </strong> - <span className="glyphicon glyphicon-chevron-down header-dropdown__icon"></span> - </a> - <ul className="dropdown-menu" role="menu" aria-labelledby="channel_header_dropdown"> - <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_invite" href="#">Add Members</a></li> - { isAdmin ? - <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_members" href="#">Manage Members</a></li> - : "" - } - <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#edit_channel" data-desc={this.state.channel.description} data-title={this.state.channel.display_name} data-channelid={this.state.channel.id}>Set Channel Description...</a></li> - <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#channel_notifications" data-title={this.state.channel.display_name} data-channelid={this.state.channel.id}>Notification Preferences</a></li> - { isAdmin && channelName != Constants.DEFAULT_CHANNEL ? - <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#rename_channel" data-display={this.state.channel.display_name} data-name={this.state.channel.name} data-channelid={this.state.channel.id}>Rename Channel...</a></li> - : "" - } - { isAdmin && channelName != Constants.DEFAULT_CHANNEL ? - <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#delete_channel" data-title={this.state.channel.display_name} data-channelid={this.state.channel.id}>Delete Channel...</a></li> - : "" - } - { channelName != Constants.DEFAULT_CHANNEL ? - <li role="presentation"><a role="menuitem" href="#" onClick={this.handleLeave}>Leave Channel</a></li> - : "" - } - </ul> + { !isDirect && channel ? + <div className="navbar-brand"> + <div className="dropdown"> + <div data-toggle="popover" data-content={popoverContent} className="description info-popover"></div> + <a href="#" className="dropdown-toggle theme" type="button" id="channel_header_dropdown" data-toggle="dropdown" aria-expanded="true"> + <span className="heading">{channelTitle} </span> + <span className="glyphicon glyphicon-chevron-down header-dropdown__icon"></span> + </a> + <ul className="dropdown-menu" role="menu" aria-labelledby="channel_header_dropdown"> + { !ChannelStore.isDefault(channel) ? + <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_invite" href="#">Add Members</a></li> + : null + } + { isAdmin && !ChannelStore.isDefault(channel) ? + <li role="presentation"><a role="menuitem" data-toggle="modal" data-target="#channel_members" href="#">Manage Members</a></li> + : null + } + <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#edit_channel" data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id}>Set Channel Description...</a></li> + <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#channel_notifications" data-title={channel.display_name} data-channelid={channel.id}>Notification Preferences</a></li> + { isAdmin && !ChannelStore.isDefault(channel) ? + <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#rename_channel" data-display={channel.display_name} data-name={channel.name} data-channelid={channel.id}>Rename Channel...</a></li> + : null + } + { isAdmin && !ChannelStore.isDefault(channel) ? + <li role="presentation"><a role="menuitem" href="#" data-toggle="modal" data-target="#delete_channel" data-title={channel.display_name} data-channelid={channel.id}>Delete Channel...</a></li> + : null + } + { !ChannelStore.isDefault(channel) ? + <li role="presentation"><a role="menuitem" href="#" onClick={this.handleLeave}>Leave Channel</a></li> + : null + } + </ul> + </div> </div> - </div> - : "" } - { isDirect && this.state.channel ? + : null + } + { isDirect && channel ? <div className="navbar-brand"> - <strong> - <a href="#"><strong className="heading">{channelTitle}</strong></a> - </strong> + <a href="#" className="heading">{ channelTitle }</a> </div> - : "" } - { !this.state.channel ? + : null } + { !channel ? <div className="navbar-brand"> - <strong> - <a href="/"><strong className="heading">{ channelTitle }</strong></a> - </strong> + <a href="/" className="heading">{ channelTitle }</a> </div> - : "" } - </div> - <div className="collapse navbar-collapse" id="navbar-collapse-1"> - { loginForm } + : null } </div> + { !currentId ? + <div className="collapse navbar-collapse" id="navbar-collapse-1"> + <NavbarLoginForm /> + </div> + : null + } </div> </nav> ); -} + } }); - - 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_body.jsx b/web/react/components/post_body.jsx index cf542a98f..d9678df30 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -71,11 +71,22 @@ module.exports = React.createClass({ name = <a className="theme" onClick={function(){ utils.searchForTerm(profile.username); }}>{profile.username}</a>; } - var message = parentPost.message; + var message = "" + if(parentPost.message) { + message = utils.replaceHtmlEntities(parentPost.message) + } else if (parentPost.filenames.length) { + message = parentPost.filenames[0].split('/').pop(); + + if (parentPost.filenames.length === 2) { + message += " plus 1 other file"; + } else if (parentPost.filenames.length > 2) { + message += " plus " + (parentPost.filenames.length - 1) + " other files"; + } + } comment = ( <p className="post-link"> - <span>Commented on {name}{apostrophe} message: <a className="theme" onClick={this.props.handleCommentClick}>{utils.replaceHtmlEntities(message)}</a></span> + <span>Commented on {name}{apostrophe} message: <a className="theme" onClick={this.props.handleCommentClick}>{message}</a></span> </p> ); diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index cf01747f0..d6422fe3a 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -11,6 +11,7 @@ module.exports = React.createClass({ render: function() { var post = this.props.post; var isOwner = UserStore.getCurrentId() == post.user_id; + var isAdmin = UserStore.getCurrentUser().roles.indexOf("admin") > -1 var type = "Post" if (post.root_id.length > 0) { @@ -30,13 +31,11 @@ module.exports = React.createClass({ <div className="dropdown"> { isOwner || (this.props.allowReply === "true" && type != "Comment") ? <div> - <a href="#" className="dropdown-toggle theme" type="button" data-toggle="dropdown" aria-expanded="false"> - [...] - </a> + <a href="#" className="dropdown-toggle theme" type="button" data-toggle="dropdown" aria-expanded="false" /> <ul className="dropdown-menu" role="menu"> { isOwner ? <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} data-comments={type === "Post" ? this.props.commentCount : 0}>Edit</a></li> : "" } - { isOwner ? <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={type === "Post" ? this.props.commentCount : 0}>Delete</a></li> + { isOwner || isAdmin ? <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={type === "Post" ? this.props.commentCount : 0}>Delete</a></li> : "" } { this.props.allowReply === "true" ? <li role="presentation"><a className="reply-link theme" href="#" onClick={this.props.handleCommentClick}>Reply</a></li> : "" } diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index 9349d0240..5439ca43d 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -6,7 +6,6 @@ var ChannelStore = require('../stores/channel_store.jsx'); var UserStore = require('../stores/user_store.jsx'); var UserProfile = require( './user_profile.jsx' ); var AsyncClient = require('../utils/async_client.jsx'); -var CreatePost = require('./create_post.jsx'); var Post = require('./post.jsx'); var LoadingScreen = require('./loading_screen.jsx'); var SocketStore = require('../stores/socket_store.jsx'); @@ -265,7 +264,7 @@ module.exports = React.createClass({ }, function(err) { $(self.refs.loadmore.getDOMNode()).text("Load more messages"); - dispatchError(err, "getPosts"); + AsyncClient.dispatchError(err, "getPosts"); } ); }, @@ -306,7 +305,7 @@ module.exports = React.createClass({ var teammate = utils.getDirectTeammate(channel.id) if (teammate) { - var teammate_name = teammate.full_name.length > 0 ? teammate.full_name : teammate.username; + var teammate_name = teammate.nickname.length > 0 ? teammate.nickname : teammate.username; more_messages = ( <div className="channel-intro"> <div className="post-profile-img__container channel-intro-img"> @@ -341,7 +340,7 @@ module.exports = React.createClass({ } } - if (channel.name === Constants.DEFAULT_CHANNEL) { + if (ChannelStore.isDefault(channel)) { more_messages = ( <div className="channel-intro"> <h4 className="channel-intro-title">Welcome</h4> diff --git a/web/react/components/post_right.jsx b/web/react/components/post_right.jsx index 408fbf83a..cc6751738 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"> @@ -129,9 +129,7 @@ RootPost = React.createClass({ <div className="dropdown"> { isOwner ? <div> - <a href="#" className="dropdown-toggle theme" type="button" data-toggle="dropdown" aria-expanded="false"> - [...] - </a> + <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> diff --git a/web/react/components/search_bar.jsx b/web/react/components/search_bar.jsx index cddb738f9..f21f0cd58 100644 --- a/web/react/components/search_bar.jsx +++ b/web/react/components/search_bar.jsx @@ -3,6 +3,7 @@ var client = require('../utils/client.jsx'); +var AsyncClient = require('../utils/async_client.jsx'); var PostStore = require('../stores/post_store.jsx'); var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var utils = require('../utils/utils.jsx'); @@ -10,14 +11,14 @@ var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; function getSearchTermStateFromStores() { - term = PostStore.getSearchTerm(); - if (!term) term = ""; + var term = PostStore.getSearchTerm() || ''; return { search_term: term }; } module.exports = React.createClass({ + displayName: 'SearchBar', componentDidMount: function() { PostStore.addSearchTermChangeListener(this._onChange); }, @@ -58,14 +59,14 @@ module.exports = React.createClass({ e.target.select(); }, performSearch: function(terms, isMentionSearch) { - if (terms.length > 0) { - $("#search-spinner").removeClass("hidden"); + if (terms.length) { + this.setState({isSearching: true}); client.search( terms, function(data) { - $("#search-spinner").addClass("hidden"); - if(utils.isMobile()) { - $('#search')[0].value = ""; + this.setState({isSearching: false}); + if (utils.isMobile()) { + React.findDOMNode(this.refs.search).value = ''; } AppDispatcher.handleServerAction({ @@ -73,18 +74,17 @@ module.exports = React.createClass({ results: data, is_mention_search: isMentionSearch }); - }, + }.bind(this), function(err) { - $("#search-spinner").addClass("hidden"); - dispatchError(err, "search"); - } + this.setState({isSearching: false}); + AsyncClient.dispatchError(err, "search"); + }.bind(this) ); } }, handleSubmit: function(e) { e.preventDefault(); - terms = this.state.search_term.trim(); - this.performSearch(terms); + this.performSearch(this.state.search_term.trim()); }, getInitialState: function() { return getSearchTermStateFromStores(); @@ -92,11 +92,18 @@ module.exports = React.createClass({ render: function() { return ( <div> - <div className="sidebar__collapse" onClick={this.handleClose}></div> + <div className="sidebar__collapse" onClick={this.handleClose}>Cancel</div> <span className="glyphicon glyphicon-search sidebar__search-icon"></span> <form role="form" className="search__form relative-div" onSubmit={this.handleSubmit}> - <input type="text" className="form-control search-bar-box" ref="search" id="search" placeholder="Search" value={this.state.search_term} onFocus={this.handleUserFocus} onChange={this.handleUserInput} /> - <span id="search-spinner" className="glyphicon glyphicon-refresh glyphicon-refresh-animate hidden"></span> + <input + type="text" + ref="search" + className="form-control search-bar-box" + placeholder="Search" + value={this.state.search_term} + onFocus={this.handleUserFocus} + onChange={this.handleUserInput} /> + {this.state.isSearching ? <span className={"glyphicon glyphicon-refresh glyphicon-refresh-animate"}></span> : null} </form> </div> ); diff --git a/web/react/components/search_results.jsx b/web/react/components/search_results.jsx index 156cf0120..1fd974642 100644 --- a/web/react/components/search_results.jsx +++ b/web/react/components/search_results.jsx @@ -8,12 +8,13 @@ var UserStore = require('../stores/user_store.jsx'); var UserProfile = require( './user_profile.jsx' ); var SearchBox =require('./search_bar.jsx'); var utils = require('../utils/utils.jsx'); -var client =require('../utils/client.jsx'); +var client = require('../utils/client.jsx'); +var AsyncClient = require('../utils/async_client.jsx'); var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; -RhsHeaderSearch = React.createClass({ +var RhsHeaderSearch = React.createClass({ handleClose: function(e) { e.preventDefault(); @@ -32,13 +33,13 @@ RhsHeaderSearch = React.createClass({ return ( <div className="sidebar--right__header"> <span className="sidebar--right__title">{title}</span> - <button type="button" className="sidebar--right__close" aria-label="Close" onClick={this.handleClose}></button> + <button type="button" className="sidebar--right__close" aria-label="Close" title="Close" onClick={this.handleClose}></button> </div> ); } }); -SearchItem = React.createClass({ +var SearchItem = React.createClass({ handleClick: function(e) { e.preventDefault(); @@ -62,15 +63,16 @@ SearchItem = React.createClass({ }); }, function(err) { - dispatchError(err, "getPost"); + AsyncClient.dispatchError(err, "getPost"); } ); - var postChannel = ChannelStore.get(this.props.post.channel_id); - var teammate = postChannel.type === 'D' ? utils.getDirectTeammate(this.props.post.channel_id).username : ""; + var postChannel = ChannelStore.get(this.props.post.channel_id); + var teammate = postChannel.type === 'D' ? utils.getDirectTeammate(this.props.post.channel_id).username : ""; - utils.switchChannel(postChannel,teammate); + utils.switchChannel(postChannel, teammate); }, + render: function() { var message = utils.textToJsx(this.props.post.message, {searchTerm: this.props.term, noMentionHighlight: !this.props.isMentionSearch}); @@ -79,14 +81,10 @@ SearchItem = React.createClass({ var timestamp = UserStore.getCurrentUser().update_at; if (channel) { - if (channel.type === 'D') { - channelName = "Private Message"; - } else { - channelName = channel.display_name; - } + channelName = (channel.type === 'D') ? "Private Message" : channel.display_name; } - return ( + return ( <div className="search-item-container post" onClick={this.handleClick}> <div className="search-channel__name">{ channelName }</div> <div className="post-profile-img__container"> @@ -95,7 +93,11 @@ SearchItem = React.createClass({ <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="search-item-time">{ utils.displayDate(this.props.post.create_at)+' '+utils.displayTime(this.props.post.create_at) }</time></li> + <li className="post-header-col"> + <time className="search-item-time"> + { utils.displayDate(this.props.post.create_at) + ' ' + utils.displayTime(this.props.post.create_at) } + </time> + </li> </ul> <div className="search-item-snippet"><span>{message}</span></div> </div> @@ -104,11 +106,13 @@ SearchItem = React.createClass({ } }); + function getStateFromStores() { return { results: PostStore.getSearchResults() }; } module.exports = React.createClass({ + displayName: 'SearchResults', componentDidMount: function() { PostStore.addSearchChangeListener(this._onChange); this.resize(); @@ -144,41 +148,24 @@ module.exports = React.createClass({ var results = this.state.results; var currentId = UserStore.getCurrentId(); - var searchForm = currentId == null ? null : <SearchBox />; - - if (results == null) { - return ( - <div className="sidebar--right__header"> - <div className="sidebar__heading">Search Results</div> - </div> - ); - } + var searchForm = currentId ? <SearchBox /> : null; + var noResults = (!results || !results.order || !results.order.length); + var searchTerm = PostStore.getSearchTerm(); - if (!results.order || results.order.length == 0) { - return ( - <div className="sidebar--right__content"> - <div className="search-bar__container">{searchForm}</div> - <div className="sidebar-right__body"> - <RhsHeaderSearch /> - <div id="search-items-container" className="search-items-container"> - <div className="sidebar--right__subheader">No results</div> - </div> - </div> - </div> - ); - } - - var self = this; return ( <div className="sidebar--right__content"> <div className="search-bar__container sidebar--right__search-header">{searchForm}</div> <div className="sidebar-right__body"> <RhsHeaderSearch isMentionSearch={this.props.isMentionSearch} /> <div id="search-items-container" className="search-items-container"> - {results.order.map(function(id) { - var post = results.posts[id]; - return <SearchItem key={post.id} post={post} term={PostStore.getSearchTerm()} isMentionSearch={self.props.isMentionSearch} /> - })} + + { noResults ? <div className="sidebar--right__subheader">No results</div> + : results.order.map(function(id) { + var post = results.posts[id]; + return <SearchItem key={post.id} post={post} term={searchTerm} isMentionSearch={this.props.isMentionSearch} /> + }, this) + } + </div> </div> </div> diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index cae9425d3..65727c597 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -131,7 +131,7 @@ function getStateFromStores() { var channel = ChannelStore.getByName(channelName); if (channel != null) { - channel.display_name = teammate.full_name.trim() != "" ? teammate.full_name : teammate.username; + channel.display_name = utils.getDisplayName(teammate); channel.teammate_username = teammate.username; channel.status = UserStore.getStatus(teammate.id); @@ -150,7 +150,7 @@ function getStateFromStores() { var tempChannel = {}; tempChannel.fake = true; tempChannel.name = channelName; - tempChannel.display_name = teammate.full_name.trim() != "" ? teammate.full_name : teammate.username; + tempChannel.display_name = utils.getDisplayName(teammate); tempChannel.status = UserStore.getStatus(teammate.id); tempChannel.last_post_at = 0; readDirectChannels.push(tempChannel); diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index 54858a04d..45c9ca629 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -37,17 +37,13 @@ var NavbarDropdown = React.createClass({ var invite_link = ""; var manage_link = ""; var rename_link = ""; - var currentUser = UserStore.getCurrentUser() + var currentUser = UserStore.getCurrentUser(); var isAdmin = false; if (currentUser != null) { isAdmin = currentUser.roles.indexOf("admin") > -1; - invite_link = ( - <li> - <a href="#" data-toggle="modal" data-target="#invite_member">Invite New Member</a> - </li> - ); + invite_link = ( <li> <a href="#" data-toggle="modal" data-target="#invite_member">Invite New Member</a> </li>); if (this.props.teamType == "O") { team_link = ( @@ -59,16 +55,8 @@ var NavbarDropdown = React.createClass({ } if (isAdmin) { - manage_link = ( - <li> - <a href="#" data-toggle="modal" data-target="#team_members">Manage Team</a> - </li> - ); - rename_link = ( - <li> - <a href="#" data-toggle="modal" data-target="#rename_team_link">Rename</a> - </li> - ); + manage_link = ( <li> <a href="#" data-toggle="modal" data-target="#team_members">Manage Team</a> </li>); + rename_link = ( <li> <a href="#" data-toggle="modal" data-target="#rename_team_link">Rename</a> </li>); } var teams = []; @@ -91,11 +79,11 @@ 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> - { isAdmin ? <li><a href="#" data-toggle="modal" data-target="#team_settings">Team Settings</a></li> : "" } + { isAdmin ? <li><a href="#" data-toggle="modal" data-target="#team_settings">Team Settings</a></li> : null } { invite_link } { team_link } { manage_link } @@ -113,28 +101,30 @@ var NavbarDropdown = React.createClass({ }); module.exports = React.createClass({ - handleSubmit: function(e) { - e.preventDefault(); - }, - getInitialState: function() { - return { }; + displayName: 'SidebarHeader', + + getDefaultProps: function() { + return { + teamName: config.SiteName + }; }, + render: function() { - var teamName = this.props.teamName ? this.props.teamName : config.SiteName; - var me = UserStore.getCurrentUser() + var me = UserStore.getCurrentUser(); + + if (!me) { + return null; + } return ( <div className="team__header theme"> <img className="user__picture" src={"/api/v1/users/" + me.id + "/image?time=" + me.update_at} /> <div className="header__info"> - <div className="user__name">@{me.username}</div> - <a className="team__name" href="/channels/town-square">{ teamName }</a> + <div className="user__name">{'@' + me.username}</div> + <a className="team__name" href="/channels/town-square">{this.props.teamName}</a> </div> <NavbarDropdown teamType={this.props.teamType} /> </div> ); } }); - - - diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index fa0c26017..d1053c778 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -46,25 +46,24 @@ module.exports = React.createClass({ function(data) { client.track('signup', 'signup_user_02_complete'); - if (data.email_verified) { - client.loginByEmail(this.props.domain, this.state.user.email, this.state.user.password, - function(data) { - UserStore.setLastDomain(this.props.domain); - UserStore.setLastEmail(this.state.user.email); - UserStore.setCurrentUser(data); - if (this.props.hash > 0) - BrowserStore.setGlobalItem(this.props.hash, {wizard: "finished"}); - window.location.href = '/channels/town-square'; - }.bind(this), - function(err) { + client.loginByEmail(this.props.domain, this.state.user.email, this.state.user.password, + function(data) { + UserStore.setLastDomain(this.props.domain); + UserStore.setLastEmail(this.state.user.email); + UserStore.setCurrentUser(data); + if (this.props.hash > 0) + BrowserStore.setGlobalItem(this.props.hash, JSON.stringify({wizard: "finished"})); + window.location.href = '/channels/town-square'; + }.bind(this), + function(err) { + if (err.message == "Login failed because email address has not been verified") { + window.location.href = "/verify?email="+ encodeURIComponent(this.state.user.email) + "&domain=" + encodeURIComponent(this.props.domain); + } else { this.state.server_error = err.message; this.setState(this.state); - }.bind(this) - ); - } - else { - window.location.href = "/verify?email="+ encodeURIComponent(this.state.user.email) + "&domain=" + encodeURIComponent(this.props.domain); - } + } + }.bind(this) + ); }.bind(this), function(err) { this.state.server_error = err.message; 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 b4c3747af..59c97c309 100644 --- a/web/react/components/user_settings.jsx +++ b/web/react/components/user_settings.jsx @@ -156,6 +156,8 @@ var NotificationsTab = React.createClass({ var self = this; + var user = this.props.user; + var desktopSection; if (this.props.activeSection === 'desktop') { var notifyActive = [false, false, false]; @@ -314,20 +316,14 @@ var NotificationsTab = React.createClass({ var keysSection; if (this.props.activeSection === 'keys') { - var user = this.props.user; - var first_name = ""; - if (user.full_name.length > 0) { - first_name = user.full_name.split(' ')[0]; - } - var inputs = []; - if (first_name != "") { + if (user.first_name) { inputs.push( <div> <div className="checkbox"> <label> - <input type="checkbox" checked={this.state.first_name_key} onChange={function(e){self.updateFirstNameKey(e.target.checked);}}>{'Your case sensitive first name "' + first_name + '"'}</input> + <input type="checkbox" checked={this.state.first_name_key} onChange={function(e){self.updateFirstNameKey(e.target.checked);}}>{'Your case sensitive first name "' + user.first_name + '"'}</input> </label> </div> </div> @@ -396,14 +392,9 @@ var NotificationsTab = React.createClass({ ); } else { var keys = []; - if (this.state.first_name_key) { - var first_name = ""; - var user = this.props.user; - if (user.full_name.length > 0) first_name = user.full_name.split(' ')[0]; - if (first_name != "") keys.push(first_name); - } - if (this.state.username_key) keys.push(this.props.user.username); - if (this.state.mention_key) keys.push('@'+this.props.user.username); + if (this.state.first_name_key) keys.push(user.first_name); + if (this.state.username_key) keys.push(user.username); + if (this.state.mention_key) keys.push('@'+user.username); if (this.state.all_key) keys.push('@all'); if (this.state.channel_key) keys.push('@channel'); if (this.state.custom_keys.length > 0) keys = keys.concat(this.state.custom_keys.split(',')); @@ -560,7 +551,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 +567,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) @@ -626,7 +617,7 @@ var SecurityTab = React.createClass({ client.updatePassword(data, function(data) { - this.updateSection(""); + this.props.updateSection(""); AsyncClient.getMe(); this.setState({ current_password: '', new_password: '', confirm_password: '' }); }.bind(this), @@ -752,6 +743,21 @@ var GeneralTab = React.createClass({ this.submitUser(user); }, + submitNickname: function(e) { + e.preventDefault(); + + var user = UserStore.getCurrentUser(); + var nickname = this.state.nickname.trim(); + + if (user.nickname === nickname) { + this.setState({client_error: "You must submit a new nickname"}) + return; + } + + user.nickname = nickname; + + this.submitUser(user); + }, submitName: function(e) { e.preventDefault(); @@ -759,14 +765,13 @@ var GeneralTab = React.createClass({ var firstName = this.state.first_name.trim(); var lastName = this.state.last_name.trim(); - var fullName = firstName + ' ' + lastName; - - if (user.full_name === fullName) { - this.setState({client_error: "You must submit a new name"}) + if (user.first_name === firstName && user.last_name === lastName) { + this.setState({client_error: "You must submit a new first or last name"}) return; } - user.full_name = fullName; + user.first_name = firstName; + user.last_name = lastName; this.submitUser(user); }, @@ -820,6 +825,7 @@ var GeneralTab = React.createClass({ client.uploadProfileImage(formData, function(data) { this.submitActive = false; + AsyncClient.getMe(); window.location.reload(); }.bind(this), function(err) { @@ -838,6 +844,9 @@ var GeneralTab = React.createClass({ updateLastName: function(e) { 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}); }, @@ -860,11 +869,7 @@ var GeneralTab = React.createClass({ getInitialState: function() { var user = this.props.user; - var splitStr = user.full_name.split(' '); - var firstName = splitStr.shift(); - var lastName = splitStr.join(' '); - - return { username: user.username, first_name: firstName, last_name: lastName, + return { username: user.username, first_name: user.first_name, last_name: user.last_name, nickname: user.nickname, email: user.email, picture: null }; }, render: function() { @@ -900,7 +905,7 @@ var GeneralTab = React.createClass({ nameSection = ( <SettingItemMax - title="Name" + title="Full Name" inputs={inputs} submit={this.submitName} server_error={server_error} @@ -909,15 +914,58 @@ var GeneralTab = React.createClass({ /> ); } else { + var full_name = ""; + + if (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) { + full_name = user.last_name; + } + nameSection = ( <SettingItemMin - title="Name" - describe={UserStore.getCurrentUser().full_name} + title="Full Name" + describe={full_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> + </div> + ); + + nicknameSection = ( + <SettingItemMax + title="Nickname" + inputs={inputs} + submit={this.submitNickname} + server_error={server_error} + client_error={client_error} + updateSection={function(e){self.updateSection("");e.preventDefault();}} + /> + ); + } else { + nicknameSection = ( + <SettingItemMin + title="Nickname" + describe={UserStore.getCurrentUser().nickname} + updateSection={function(){self.updateSection("nickname");}} + /> + ); + } + var usernameSection; if (this.props.activeSection === 'username') { var inputs = []; @@ -989,7 +1037,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 +1048,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");}} /> ); @@ -1021,6 +1073,8 @@ var GeneralTab = React.createClass({ <div className="divider-light"/> {usernameSection} <div className="divider-light"/> + {nicknameSection} + <div className="divider-light"/> {emailSection} <div className="divider-light"/> {pictureSection} diff --git a/web/react/stores/channel_store.jsx b/web/react/stores/channel_store.jsx index 4a27e5f17..a97f13391 100644 --- a/web/react/stores/channel_store.jsx +++ b/web/react/stores/channel_store.jsx @@ -44,40 +44,24 @@ var ChannelStore = assign({}, EventEmitter.prototype, { removeExtraInfoChangeListener: function(callback) { this.removeListener(EXTRA_INFO_EVENT, callback); }, - get: function(id) { - var current = null; - var c = this._getChannels(); - - c.some(function(channel) { - if (channel.id == id) { - current = channel; - return true; + findFirstBy: function(field, value) { + var channels = this._getChannels(); + for (var i = 0; i < channels.length; i++) { + if (channels[i][field] == value) { + return channels[i]; } - return false; - }); + } - return current; + return null; + }, + get: function(id) { + return this.findFirstBy('id', id); }, getMember: function(id) { - var current = null; return this.getAllMembers()[id]; }, getByName: function(name) { - var current = null; - var c = this._getChannels(); - - c.some(function(channel) { - if (channel.name == name) { - current = channel; - return true; - } - - return false; - - }); - - return current; - + return this.findFirstBy('name', name); }, getAll: function() { return this._getChannels(); @@ -120,7 +104,7 @@ var ChannelStore = assign({}, EventEmitter.prototype, { getCurrent: function() { var currentId = this.getCurrentId(); - if (currentId != null) + if (currentId) return this.get(currentId); else return null; @@ -128,7 +112,7 @@ var ChannelStore = assign({}, EventEmitter.prototype, { getCurrentMember: function() { var currentId = ChannelStore.getCurrentId(); - if (currentId != null) + if (currentId) return this.getAllMembers()[currentId]; else return null; @@ -143,7 +127,7 @@ var ChannelStore = assign({}, EventEmitter.prototype, { var currentId = ChannelStore.getCurrentId(); var extra = null; - if (currentId != null) + if (currentId) extra = this._getExtraInfos()[currentId]; if (extra == null) @@ -154,7 +138,7 @@ var ChannelStore = assign({}, EventEmitter.prototype, { getExtraInfo: function(channel_id) { var extra = null; - if (channel_id != null) + if (channel_id) extra = this._getExtraInfos()[channel_id]; if (extra == null) @@ -192,7 +176,10 @@ var ChannelStore = assign({}, EventEmitter.prototype, { }, _getExtraInfos: function() { return BrowserStore.getItem("extra_infos", {}); - } + }, + isDefault: function(channel) { + return channel.name == Constants.DEFAULT_CHANNEL; + } }); ChannelStore.dispatchToken = AppDispatcher.register(function(payload) { @@ -231,4 +218,4 @@ ChannelStore.dispatchToken = AppDispatcher.register(function(payload) { } }); -module.exports = ChannelStore; +module.exports = ChannelStore;
\ No newline at end of file diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index 93ddfec70..b0ea719d4 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -177,21 +177,15 @@ var UserStore = assign({}, EventEmitter.prototype, { }, getCurrentMentionKeys: function() { var user = this.getCurrentUser(); - if (user.notify_props && user.notify_props.mention_keys) { - var keys = user.notify_props.mention_keys.split(','); - if (user.full_name.length > 0 && user.notify_props.first_name === "true") { - var first = user.full_name.split(' ')[0]; - if (first.length > 0) keys.push(first); - } + var keys = []; - if (user.notify_props.all === "true") keys.push('@all'); - if (user.notify_props.channel === "true") keys.push('@channel'); + if (user.notify_props && user.notify_props.mention_keys) keys = keys.concat(user.notify_props.mention_keys.split(',')); + if (user.first_name && user.notify_props.first_name === "true") keys.push(user.first_name); + if (user.notify_props.all === "true") keys.push('@all'); + if (user.notify_props.channel === "true") keys.push('@channel'); - return keys; - } else { - return []; - } + return keys; }, getLastVersion: function() { return BrowserStore.getItem("last_version", ''); diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index 19c074606..416ea5ae4 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"> @@ -783,7 +796,6 @@ module.exports.getHomeLink = function() { return window.location.protocol + "//" + parts.join("."); } - module.exports.changeColor =function(col, amt) { var usePound = false; @@ -811,5 +823,30 @@ module.exports.changeColor =function(col, amt) { else if (g < 0) g = 0; return (usePound?"#":"") + String("000000" + (g | (b << 8) | (r << 16)).toString(16)).slice(-6); +}; +module.exports.getFullName = function(user) { + if (user.first_name && user.last_name) { + return user.first_name + " " + user.last_name; + } else if (user.first_name) { + return user.first_name; + } else if (user.last_name) { + return user.last_name; + } else { + return ""; + } +}; + +module.exports.getDisplayName = function(user) { + if (user.nickname && user.nickname.trim().length > 0) { + return user.nickname; + } else { + var fullName = module.exports.getFullName(user); + + if (fullName) { + return fullName; + } else { + return user.username; + } + } }; diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss index fd6225bdd..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 { @@ -126,6 +132,10 @@ div.theme { to { transform: scale(1) rotate(360deg);} } +.glyphicon-refresh-animate { + @include animation(spin .7s infinite linear); +} + .black-bg { background-color: black !important; } diff --git a/web/sass-files/sass/partials/_files.scss b/web/sass-files/sass/partials/_files.scss index 79142176e..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%; } @@ -137,7 +138,7 @@ border: 1px solid #E2E2E2; background-color: #FFF; background-repeat: no-repeat; - background-position: left center; + background-position: top left; } a { text-decoration: none; diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss index 7b0f24abf..c2740891a 100644 --- a/web/sass-files/sass/partials/_headers.scss +++ b/web/sass-files/sass/partials/_headers.scss @@ -75,7 +75,7 @@ // Team Header in Sidebar .sidebar--left, .sidebar--menu { .team__header { - padding: 15px; + padding: 10px; @include legacy-pie-clearfix; a { color: #fff; @@ -83,8 +83,8 @@ .navbar-right { font-size: 0.85em; position: absolute; - top: 24px; - right: 25px; + top: 20px; + right: 22px; .dropdown-toggle { padding: 0 10px; } @@ -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 { @@ -161,9 +157,10 @@ font-size: 14px; line-height: 50px; #member_popover { + margin-right: 5px; width: 45px; color: #999; - + cursor: pointer; } &.alt { margin: 0; @@ -230,3 +227,25 @@ top: 1px; } } + +.channel-header__links { + height: 32px; + vertical-align: top; + display: inline-block; + width: 15px; + margin: 9px 4px 3px 0; + &:hover { + svg { + fill: #888; + } + } + a { + height: 100%; + display: block; + } + 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..f359037c5 100644 --- a/web/sass-files/sass/partials/_modal.scss +++ b/web/sass-files/sass/partials/_modal.scss @@ -154,7 +154,8 @@ background: #FFF; position: relative; max-width: 90%; - min-width: 280px; + min-height: 50px; + min-width: 320px; @include border-radius(3px); display: table; margin: 0 auto; diff --git a/web/sass-files/sass/partials/_navbar.scss b/web/sass-files/sass/partials/_navbar.scss index 62864afb7..6d8f11ce3 100644 --- a/web/sass-files/sass/partials/_navbar.scss +++ b/web/sass-files/sass/partials/_navbar.scss @@ -43,6 +43,7 @@ font-size: 16px; .heading { margin-right: 3px; + font-weight: 600; color: #fff; } .header-dropdown__icon { 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..9368786d1 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -208,6 +208,12 @@ body.ios { .dropdown, .comment-icon__container { @include opacity(1); } + .dropdown-toggle:after { + content: '...'; + } + .dropdown-toggle:hover:after { + content: '[...]'; + } } background: #f5f5f5; } @@ -425,4 +431,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 9c0c09ee3..a33d69378 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 { @@ -432,14 +432,16 @@ color: #fff; .search__form { border: none; - padding: 0 10px 0 30px; + padding: 0 60px 0 25px; .form-control { + line-height: 31px; background: none; color: #fff; - border-bottom: 1px solid #fff; - border-bottom: 1px solid rgba(#fff, 0.7); border-radius: 0; - padding: 0 0 0 23px; + padding: 0 10px 0; + @include input-placeholder { + color: rgba(#fff, 0.6); + } } ::-webkit-input-placeholder { color: #fff; @@ -534,6 +536,11 @@ .sidebar--right__close { display: none; } + .search__form { + .glyphicon { + color: #fff; + } + } } .inner__wrap { &.move--right { @@ -570,6 +577,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/_search.scss b/web/sass-files/sass/partials/_search.scss index 8d51d00c0..d4a4da243 100644 --- a/web/sass-files/sass/partials/_search.scss +++ b/web/sass-files/sass/partials/_search.scss @@ -2,21 +2,20 @@ padding: 8px 8px 8px 0; } .sidebar__collapse { - width: 20px; - height: 30px; + width: auto; + height: auto; position: absolute; - top: 10px; - left: 6px; + top: 17px; + right: 15px; cursor: pointer; - background: url("../images/arrow-left.png") center no-repeat; - @include background-size(10px 15px); z-index: 5; display: none; } .sidebar__search-icon { position: absolute; - left: 40px; + left: 15px; top: 18px; + font-size: 16px; @include opacity(0.8); display: none; } 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/partials/_variables.scss b/web/sass-files/sass/partials/_variables.scss index eb1f3eef3..5d883ab44 100644 --- a/web/sass-files/sass/partials/_variables.scss +++ b/web/sass-files/sass/partials/_variables.scss @@ -7,10 +7,4 @@ $primary-color: #2389D7; $primary-color--hover: darken(#2389D7, 5%); $body-bg: #e9e9e9; $header-bg: #f9f9f9; -$border-gray: 1px solid #ddd; - -// Animation -.glyphicon-refresh-animate { - -animation: spin .7s infinite linear; - -webkit-animation: spin2 .7s infinite linear; -}
\ No newline at end of file +$border-gray: 1px solid #ddd;
\ No newline at end of file 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 Binary files differdeleted file mode 100644 index 5c271cfc7..000000000 --- a/web/static/images/dropdown-icon.png +++ /dev/null diff --git a/web/templates/head.html b/web/templates/head.html index ead648571..0cbda28c5 100644 --- a/web/templates/head.html +++ b/web/templates/head.html @@ -31,7 +31,6 @@ <link rel="stylesheet" href="/static/css/styles.css"> <script src="/static/js/perfect-scrollbar-0.6.3.jquery.js"></script> - <script src="/static/js/bundle.js"></script> <script type="text/javascript" src="https://www.google.com/jsapi?autoload={'modules':[{'name':'visualization','version':'1','packages':['annotationchart']}]}"></script> @@ -60,7 +59,7 @@ var user = window.UserStore.getCurrentUser(true); if (user) { analytics.identify(user.id, { - name: user.full_name, + name: user.nickname, email: user.email, createdAt: user.create_at, username: user.username, @@ -102,5 +101,7 @@ } </script> <!-- Snowplow stops plowing --> + + <script src="/static/js/bundle.js"></script> </head> {{end}} diff --git a/web/web.go b/web/web.go index e771157d8..b11e6e6b1 100644 --- a/web/web.go +++ b/web/web.go @@ -303,8 +303,8 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { //api.Handle404(w, r) //Bad channel urls just redirect to the town-square for now - - http.Redirect(w,r,"/channels/town-square", http.StatusFound) + + http.Redirect(w, r, "/channels/town-square", http.StatusFound) return } } @@ -351,7 +351,7 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { return } else { user := result.Data.(*model.User) - api.FireAndForgetVerifyEmail(user.Id, strings.Split(user.FullName, " ")[0], user.Email, domain, c.TeamUrl) + api.FireAndForgetVerifyEmail(user.Id, user.FirstName, user.Email, domain, c.TeamUrl) http.Redirect(w, r, "/", http.StatusFound) return } |