diff options
40 files changed, 309 insertions, 228 deletions
diff --git a/api/context.go b/api/context.go index bea0fbeff..054e42e2e 100644 --- a/api/context.go +++ b/api/context.go @@ -265,6 +265,16 @@ func (c *Context) IsSystemAdmin() bool { return false } +func (c *Context) IsTeamAdmin(userId string) bool { + if uresult := <-Srv.Store.User().Get(userId); uresult.Err != nil { + c.Err = uresult.Err + return false + } else { + user := uresult.Data.(*model.User) + return strings.Contains(c.Session.Roles, model.ROLE_ADMIN) && user.TeamId == c.Session.TeamId + } +} + func (c *Context) RemoveSessionCookie(w http.ResponseWriter) { sessionCache.Remove(c.Session.Id) diff --git a/api/post.go b/api/post.go index 02f997166..efca2f570 100644 --- a/api/post.go +++ b/api/post.go @@ -634,16 +634,17 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId) pchan := Srv.Store.Post().Get(postId) - if !c.HasPermissionsToChannel(cchan, "deletePost") { - return - } - if result := <-pchan; result.Err != nil { c.Err = result.Err return } else { + post := result.Data.(*model.PostList).Posts[postId] + if !c.HasPermissionsToChannel(cchan, "deletePost") && !c.IsTeamAdmin(post.UserId){ + return + } + if post == nil { c.SetInvalidParam("deletePost", "postId") return @@ -655,7 +656,7 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { return } - if post.UserId != c.Session.UserId { + if post.UserId != c.Session.UserId && !strings.Contains(c.Session.Roles,model.ROLE_ADMIN) { c.Err = model.NewAppError("deletePost", "You do not have the appropriate permissions", "") c.Err.StatusCode = http.StatusForbidden return diff --git a/api/post_test.go b/api/post_test.go index 970307759..5009ff54d 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -483,6 +483,10 @@ func TestDeletePosts(t *testing.T) { team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + userAdmin := &model.User{TeamId: team.Id, Email: team.Email, FullName: "Corey Hulen", Password: "pwd"} + userAdmin = Client.Must(Client.CreateUser(userAdmin, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(userAdmin.Id)) + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) store.Must(Srv.Store.User().VerifyEmail(user1.Id)) @@ -521,8 +525,16 @@ func TestDeletePosts(t *testing.T) { r2 := Client.Must(Client.GetPosts(channel1.Id, 0, 10, "")).Data.(*model.PostList) if len(r2.Posts) != 4 { - t.Fatal("should have returned 5 items") + t.Fatal("should have returned 4 items") } + + time.Sleep(10 * time.Millisecond) + post4 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post4 = Client.Must(Client.CreatePost(post4)).Data.(*model.Post) + + Client.LoginByEmail(team.Domain, userAdmin.Email, "pwd") + + Client.Must(Client.DeletePost(channel1.Id, post4.Id)) } func TestEmailMention(t *testing.T) { diff --git a/api/templates/reset_body.html b/api/templates/reset_body.html index 3a5d62ab4..4b5976663 100644 --- a/api/templates/reset_body.html +++ b/api/templates/reset_body.html @@ -18,7 +18,7 @@ <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> <h2 style="font-weight: normal; margin-top: 10px;">You requested a password reset</h2> - <p>To change your password, click below.<br>If you did not mean to reset your password, please ignore this email and your password will remain the same.</p> + <p>To change your password, click "Reset Password" below.<br>If you did not mean to reset your password, please ignore this email and your password will remain the same.</p> <p style="margin: 20px 0 15px"> <a href="{{.Props.ResetUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Reset Password</a> </p> 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 fe381a59e..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> @@ -142,10 +142,10 @@ module.exports = React.createClass({ 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 = this.state.channel.display_name; - 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 isDirect = (this.state.channel.type === 'D'); @@ -169,23 +169,26 @@ 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={channelTitle} 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={channelTitle} 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={channelTitle} 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={channelTitle} 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 } @@ -197,14 +200,13 @@ module.exports = React.createClass({ <a href="#"><strong className="heading">{channelTitle}</strong></a> } </th> - <th><PopoverListMembers members={this.state.users} channelId={this.state.channel.id} /></th> + <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/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_info.jsx b/web/react/components/post_info.jsx index cf01747f0..48efa95ba 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) { @@ -36,7 +37,7 @@ module.exports = React.createClass({ <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 98491976f..573799a19 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'); @@ -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..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/search_bar.jsx b/web/react/components/search_bar.jsx index ab7e99d60..f21f0cd58 100644 --- a/web/react/components/search_bar.jsx +++ b/web/react/components/search_bar.jsx @@ -92,7 +92,7 @@ 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 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/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/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..dd207ca80 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -177,7 +177,7 @@ var UserStore = assign({}, EventEmitter.prototype, { }, getCurrentMentionKeys: function() { var user = this.getCurrentUser(); - if (user.notify_props && user.notify_props.mention_keys) { + if (user && 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") { 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..c2740891a 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 { @@ -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/_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..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..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,13 +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 rgba(#fff, 0.7); border-radius: 0; - padding: 0 10px 0 23px; + padding: 0 10px 0; + @include input-placeholder { + color: rgba(#fff, 0.6); + } } ::-webkit-input-placeholder { color: #fff; @@ -574,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/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..5b423d487 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> @@ -102,5 +101,7 @@ } </script> <!-- Snowplow stops plowing --> + + <script src="/static/js/bundle.js"></script> </head> {{end}} |