diff options
40 files changed, 523 insertions, 505 deletions
diff --git a/api/team.go b/api/team.go index 7d746d922..862970887 100644 --- a/api/team.go +++ b/api/team.go @@ -510,16 +510,14 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str } subjectPage := NewServerTemplatePage("invite_subject") - subjectPage.Props["SiteURL"] = c.GetSiteURL() subjectPage.Props["SenderName"] = sender subjectPage.Props["TeamDisplayName"] = team.DisplayName bodyPage := NewServerTemplatePage("invite_body") - bodyPage.Props["SiteURL"] = c.GetSiteURL() + bodyPage.Props["TeamURL"] = c.GetTeamURL() bodyPage.Props["TeamDisplayName"] = team.DisplayName bodyPage.Props["SenderName"] = sender bodyPage.Props["SenderStatus"] = senderRole - bodyPage.Props["Email"] = invite props := make(map[string]string) props["email"] = invite props["id"] = team.Id diff --git a/api/templates/invite_body.html b/api/templates/invite_body.html index 930bc099d..d98f91357 100644 --- a/api/templates/invite_body.html +++ b/api/templates/invite_body.html @@ -18,10 +18,12 @@ <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> <h2 style="font-weight: normal; margin-top: 10px;">You've been invited</h2> - <p>{{.Props.TeamDisplayName}} started using {{.ClientCfg.SiteName}}.<br> The team {{.Props.SenderStatus}} <strong>{{.Props.SenderName}}</strong>, has invited you to join <strong>{{.Props.TeamDisplayName}}</strong>.</p> - <p style="margin: 20px 0 15px"> + <p>The team {{.Props.SenderStatus}} <strong>{{.Props.SenderName}}</strong>, has invited you to join <strong>{{.Props.TeamDisplayName}}</strong>.</p> + <p style="margin: 30px 0 15px"> <a href="{{.Props.Link}}" 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;">Join Team</a> </p> + <br/> + <p>Mattermost lets you share messages and files from your PC or phone, with instant search and archiving. After you’ve joined <strong>{{.Props.TeamDisplayName}}</strong>, you can sign-in to your new team and access these features anytime from the web address:<br/><br/><a href="{{.Props.TeamURL}}">{{.Props.TeamURL}}</a></p> </td> </tr> <tr> diff --git a/api/user.go b/api/user.go index 42d3a43e7..c871d7c79 100644 --- a/api/user.go +++ b/api/user.go @@ -87,6 +87,8 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { hash := r.URL.Query().Get("h") + sendWelcomeEmail := true + if IsVerifyHashRequired(user, team, hash) { data := r.URL.Query().Get("d") props := model.MapFromJson(strings.NewReader(data)) @@ -109,6 +111,7 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { user.Email = props["email"] user.EmailVerified = true + sendWelcomeEmail = false } if len(user.AuthData) > 0 && len(user.AuthService) > 0 { @@ -120,6 +123,10 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { return } + if sendWelcomeEmail { + sendWelcomeEmailAndForget(ruser.Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), ruser.EmailVerified) + } + w.Write([]byte(ruser.ToJson())) } @@ -198,8 +205,6 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User { l4g.Error("Encountered an issue joining default channels user_id=%s, team_id=%s, err=%v", ruser.Id, ruser.TeamId, err) } - sendWelcomeEmailAndForget(ruser.Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), user.EmailVerified) - addDirectChannelsAndForget(ruser) if user.EmailVerified { diff --git a/doc/help/Search.md b/doc/help/Search.md index 02ecf7d40..51095aac6 100644 --- a/doc/help/Search.md +++ b/doc/help/Search.md @@ -2,14 +2,16 @@ The search box in Mattermost brings back results from any channel of which you’re a member. No results are returned from channels where you are not a member - even if they are open channels. -Some things to know about search: +#### Some things to know about search: - Multiple search terms are connected with “OR” by default. Typing in `Mattermost website` returns results containing “Mattermost” or “website” -- You can use quotes to return search results for exact terms, like `"Mattermost website"` will only return messages containing the entire phrase `"Mattermost website"` and not return messages with only `Mattermost` or `website` -- You can use the `*` character for wildcard searches that match within words. For example: Searching for `rea*` brings back messages containing `reach`, `reason` and other words starting with `rea`. +- Use `from:` to find posts from specific users and `in:` to find posts in specific channels. For example: Searching `Mattermost in:town-square` only returns messages in Town Square that contain `Mattermost` +- Use quotes to return search results for exact terms. For example: Searching `"Mattermost website"` returns messages containing the entire phrase `"Mattermost website"` and not messages containing only `Mattermost` or `website` +- Use the `*` character for wildcard searches that match within words. For example: Searching for `rea*` brings back messages containing `reach`, `reason` and other words starting with `rea`. -#### Limitations +#### Limitations: - Search in Mattermost uses the full text search features included in either a MySQL or Postgres database, which has some limitations - Special cases that are not supported in default full text search, such as searching for IP addresses like `10.100.200.101`, can be added in future as the search feature evolves - - Searches with fewer than three characters will return no results, so for searching in Chinese try adding * to the end of queries + - Two letter searches and common words like "this", "a" and "is" won't appear in search results + - For searching in Chinese try adding * to the end of queries diff --git a/model/channel.go b/model/channel.go index ac54a7e44..0ce09f4bc 100644 --- a/model/channel.go +++ b/model/channel.go @@ -6,6 +6,7 @@ package model import ( "encoding/json" "io" + "unicode/utf8" ) const ( @@ -74,7 +75,7 @@ func (o *Channel) IsValid() *AppError { return NewAppError("Channel.IsValid", "Update at must be a valid time", "id="+o.Id) } - if len(o.DisplayName) > 64 { + if utf8.RuneCountInString(o.DisplayName) > 64 { return NewAppError("Channel.IsValid", "Invalid display name", "id="+o.Id) } @@ -90,11 +91,11 @@ func (o *Channel) IsValid() *AppError { return NewAppError("Channel.IsValid", "Invalid type", "id="+o.Id) } - if len(o.Header) > 1024 { + if utf8.RuneCountInString(o.Header) > 1024 { return NewAppError("Channel.IsValid", "Invalid header", "id="+o.Id) } - if len(o.Purpose) > 128 { + if utf8.RuneCountInString(o.Purpose) > 128 { return NewAppError("Channel.IsValid", "Invalid purpose", "id="+o.Id) } diff --git a/model/oauth.go b/model/oauth.go index 0320e7ec7..67825dd97 100644 --- a/model/oauth.go +++ b/model/oauth.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "io" + "unicode/utf8" ) type OAuthApp struct { @@ -57,7 +58,7 @@ func (a *OAuthApp) IsValid() *AppError { return NewAppError("OAuthApp.IsValid", "Invalid homepage", "app_id="+a.Id) } - if len(a.Description) > 512 { + if utf8.RuneCountInString(a.Description) > 512 { return NewAppError("OAuthApp.IsValid", "Invalid description", "app_id="+a.Id) } diff --git a/model/post.go b/model/post.go index 11f3ad0d5..e0074b348 100644 --- a/model/post.go +++ b/model/post.go @@ -6,6 +6,7 @@ package model import ( "encoding/json" "io" + "unicode/utf8" ) const ( @@ -94,11 +95,11 @@ func (o *Post) IsValid() *AppError { return NewAppError("Post.IsValid", "Invalid original id", "") } - if len(o.Message) > 4000 { + if utf8.RuneCountInString(o.Message) > 4000 { return NewAppError("Post.IsValid", "Invalid message", "id="+o.Id) } - if len(o.Hashtags) > 1000 { + if utf8.RuneCountInString(o.Hashtags) > 1000 { return NewAppError("Post.IsValid", "Invalid hashtags", "id="+o.Id) } @@ -106,7 +107,7 @@ func (o *Post) IsValid() *AppError { return NewAppError("Post.IsValid", "Invalid type", "id="+o.Type) } - if len(ArrayToJson(o.Filenames)) > 4000 { + if utf8.RuneCountInString(ArrayToJson(o.Filenames)) > 4000 { return NewAppError("Post.IsValid", "Invalid filenames", "id="+o.Id) } diff --git a/model/preference.go b/model/preference.go index 44279f71a..bcd0237f1 100644 --- a/model/preference.go +++ b/model/preference.go @@ -6,6 +6,7 @@ package model import ( "encoding/json" "io" + "unicode/utf8" ) const ( @@ -52,7 +53,7 @@ func (o *Preference) IsValid() *AppError { return NewAppError("Preference.IsValid", "Invalid name", "name="+o.Name) } - if len(o.Value) > 128 { + if utf8.RuneCountInString(o.Value) > 128 { return NewAppError("Preference.IsValid", "Value is too long", "value="+o.Value) } diff --git a/model/team.go b/model/team.go index 5c9cf5a26..e7dde4766 100644 --- a/model/team.go +++ b/model/team.go @@ -9,6 +9,7 @@ import ( "io" "regexp" "strings" + "unicode/utf8" ) const ( @@ -122,7 +123,7 @@ func (o *Team) IsValid(restrictTeamNames bool) *AppError { return NewAppError("Team.IsValid", "Invalid email", "id="+o.Id) } - if len(o.DisplayName) == 0 || len(o.DisplayName) > 64 { + if utf8.RuneCountInString(o.DisplayName) == 0 || utf8.RuneCountInString(o.DisplayName) > 64 { return NewAppError("Team.IsValid", "Invalid name", "id="+o.Id) } diff --git a/model/user.go b/model/user.go index 15016f8dc..871d1bf2d 100644 --- a/model/user.go +++ b/model/user.go @@ -10,6 +10,7 @@ import ( "io" "regexp" "strings" + "unicode/utf8" ) const ( @@ -80,15 +81,15 @@ func (u *User) IsValid() *AppError { return NewAppError("User.IsValid", "Invalid email", "user_id="+u.Id) } - if len(u.Nickname) > 64 { + if utf8.RuneCountInString(u.Nickname) > 64 { return NewAppError("User.IsValid", "Invalid nickname", "user_id="+u.Id) } - if len(u.FirstName) > 64 { + if utf8.RuneCountInString(u.FirstName) > 64 { return NewAppError("User.IsValid", "Invalid first name", "user_id="+u.Id) } - if len(u.LastName) > 64 { + if utf8.RuneCountInString(u.LastName) > 64 { return NewAppError("User.IsValid", "Invalid last name", "user_id="+u.Id) } diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index 6a24870f6..ef3077470 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -167,8 +167,8 @@ export default class ActivityLogModal extends React.Component { <Modal.Header closeButton={true}> <Modal.Title>{'Active Sessions'}</Modal.Title> </Modal.Header> - <p className='session-help-text'>{'Sessions are created when you log in with your email and password to a new browser on a device. Sessions let you use Mattermost for up to 30 days without having to log in again. If you want to log out sooner, use the \'Logout\' button below to end a session.'}</p> <Modal.Body ref='modalBody'> + <p className='session-help-text'>{'Sessions are created when you log in with your email and password to a new browser on a device. Sessions let you use Mattermost for up to 30 days without having to log in again. If you want to log out sooner, use the \'Logout\' button below to end a session.'}</p> {content} </Modal.Body> </Modal> diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 20f106f30..895dc5fe4 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -1,20 +1,23 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -const ChannelStore = require('../stores/channel_store.jsx'); -const UserStore = require('../stores/user_store.jsx'); -const SearchStore = require('../stores/search_store.jsx'); -const PreferenceStore = require('../stores/preference_store.jsx'); const NavbarSearchBox = require('./search_bar.jsx'); -const AsyncClient = require('../utils/async_client.jsx'); -const Client = require('../utils/client.jsx'); -const TextFormatting = require('../utils/text_formatting.jsx'); -const Utils = require('../utils/utils.jsx'); const MessageWrapper = require('./message_wrapper.jsx'); const PopoverListMembers = require('./popover_list_members.jsx'); const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx'); +const ChannelInviteModal = require('./channel_invite_modal.jsx'); +const ChannelMembersModal = require('./channel_members_modal.jsx'); + +const ChannelStore = require('../stores/channel_store.jsx'); +const UserStore = require('../stores/user_store.jsx'); +const SearchStore = require('../stores/search_store.jsx'); +const PreferenceStore = require('../stores/preference_store.jsx'); const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +const Utils = require('../utils/utils.jsx'); +const TextFormatting = require('../utils/text_formatting.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); +const Client = require('../utils/client.jsx'); const Constants = require('../utils/constants.jsx'); const ActionTypes = Constants.ActionTypes; @@ -31,6 +34,8 @@ export default class ChannelHeader extends React.Component { const state = this.getStateFromStores(); state.showEditChannelPurposeModal = false; + state.showInviteModal = false; + state.showMembersModal = false; this.state = state; } getStateFromStores() { @@ -86,7 +91,7 @@ export default class ChannelHeader extends React.Component { let terms = ''; if (user.notify_props && user.notify_props.mention_keys) { - let termKeys = UserStore.getCurrentMentionKeys(); + const termKeys = UserStore.getCurrentMentionKeys(); if (user.notify_props.all === 'true' && termKeys.indexOf('@all') !== -1) { termKeys.splice(termKeys.indexOf('@all'), 1); } @@ -146,7 +151,7 @@ export default class ChannelHeader extends React.Component { channelTerm = 'Group'; } - let dropdownContents = []; + const dropdownContents = []; if (isDirect) { dropdownContents.push( <li @@ -162,7 +167,7 @@ export default class ChannelHeader extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - Set Channel Header... + {'Set Channel Header...'} </a> </li> ); @@ -179,7 +184,7 @@ export default class ChannelHeader extends React.Component { data-channelid={channel.id} href='#' > - View Info + {'View Info'} </a> </li> ); @@ -192,11 +197,10 @@ export default class ChannelHeader extends React.Component { > <a role='menuitem' - data-toggle='modal' - data-target='#channel_invite' href='#' + onClick={() => this.setState({showInviteModal: true})} > - Add Members + {'Add Members'} </a> </li> ); @@ -209,11 +213,10 @@ export default class ChannelHeader extends React.Component { > <a role='menuitem' - data-toggle='modal' - data-target='#channel_members' href='#' + onClick={() => this.setState({showMembersModal: true})} > - Manage Members + {'Manage Members'} </a> </li> ); @@ -234,7 +237,7 @@ export default class ChannelHeader extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - Set {channelTerm} Header... + {'Set '}{channelTerm}{' Header...'} </a> </li> ); @@ -248,7 +251,7 @@ export default class ChannelHeader extends React.Component { href='#' onClick={() => this.setState({showEditChannelPurposeModal: true})} > - Set {channelTerm} Purpose... + {'Set '}{channelTerm}{' Purpose...'} </a> </li> ); @@ -265,7 +268,7 @@ export default class ChannelHeader extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - Notification Preferences + {'Notification Preferences'} </a> </li> ); @@ -286,7 +289,7 @@ export default class ChannelHeader extends React.Component { data-name={channel.name} data-channelid={channel.id} > - Rename {channelTerm}... + {'Rename '}{channelTerm}{'...'} </a> </li> ); @@ -303,7 +306,7 @@ export default class ChannelHeader extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - Delete {channelTerm}... + {'Delete '}{channelTerm}{'...'} </a> </li> ); @@ -319,7 +322,7 @@ export default class ChannelHeader extends React.Component { href='#' onClick={this.handleLeave} > - Leave {channelTerm} + {'Leave '}{channelTerm} </a> </li> ); @@ -397,7 +400,7 @@ export default class ChannelHeader extends React.Component { href='#' onClick={this.searchMentions} > - Recent Mentions + {'Recent Mentions'} </a> </li> </ul> @@ -411,6 +414,14 @@ export default class ChannelHeader extends React.Component { onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})} channel={channel} /> + <ChannelInviteModal + show={this.state.showInviteModal} + onModalDismissed={() => this.setState({showInviteModal: false})} + /> + <ChannelMembersModal + show={this.state.showMembersModal} + onModalDismissed={() => this.setState({showMembersModal: false})} + /> </div> ); } diff --git a/web/react/components/channel_invite_modal.jsx b/web/react/components/channel_invite_modal.jsx index e90d1a666..2dc12c9aa 100644 --- a/web/react/components/channel_invite_modal.jsx +++ b/web/react/components/channel_invite_modal.jsx @@ -1,26 +1,25 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var UserStore = require('../stores/user_store.jsx'); -var ChannelStore = require('../stores/channel_store.jsx'); -var MemberList = require('./member_list.jsx'); -var LoadingScreen = require('./loading_screen.jsx'); -var utils = require('../utils/utils.jsx'); -var client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); +const MemberList = require('./member_list.jsx'); +const LoadingScreen = require('./loading_screen.jsx'); + +const UserStore = require('../stores/user_store.jsx'); +const ChannelStore = require('../stores/channel_store.jsx'); + +const Utils = require('../utils/utils.jsx'); +const Client = require('../utils/client.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); + +const Modal = ReactBootstrap.Modal; export default class ChannelInviteModal extends React.Component { constructor() { super(); - this.componentDidMount = this.componentDidMount.bind(this); - this.componentWillUnmount = this.componentWillUnmount.bind(this); - this.onShow = this.onShow.bind(this); - this.onHide = this.onHide.bind(this); this.onListenerChange = this.onListenerChange.bind(this); this.handleInvite = this.handleInvite.bind(this); - this.isShown = false; this.state = this.getStateFromStores(); } getStateFromStores() { @@ -39,7 +38,7 @@ export default class ChannelInviteModal extends React.Component { } } - nonmembers.sort(function sortByUsername(a, b) { + nonmembers.sort((a, b) => { return a.username.localeCompare(b.username); }); @@ -49,35 +48,27 @@ export default class ChannelInviteModal extends React.Component { } return { - nonmembers: nonmembers, - memberIds: memberIds, - channelName: channelName, - loading: loading + nonmembers, + memberIds, + channelName, + loading }; } - componentDidMount() { - $(ReactDOM.findDOMNode(this)).on('hidden.bs.modal', this.onHide); - $(ReactDOM.findDOMNode(this)).on('show.bs.modal', this.onShow); - - ChannelStore.addExtraInfoChangeListener(this.onListenerChange); - ChannelStore.addChangeListener(this.onListenerChange); - UserStore.addChangeListener(this.onListenerChange); - } - componentWillUnmount() { - ChannelStore.removeExtraInfoChangeListener(this.onListenerChange); - ChannelStore.removeChangeListener(this.onListenerChange); - UserStore.removeChangeListener(this.onListenerChange); - } - onShow() { - this.isShown = true; - this.onListenerChange(); - } - onHide() { - this.isShown = false; + componentWillReceiveProps(nextProps) { + if (!this.props.show && nextProps.show) { + ChannelStore.addExtraInfoChangeListener(this.onListenerChange); + ChannelStore.addChangeListener(this.onListenerChange); + UserStore.addChangeListener(this.onListenerChange); + this.onListenerChange(); + } else if (this.props.show && !nextProps.show) { + ChannelStore.removeExtraInfoChangeListener(this.onListenerChange); + ChannelStore.removeChangeListener(this.onListenerChange); + UserStore.removeChangeListener(this.onListenerChange); + } } onListenerChange() { var newState = this.getStateFromStores(); - if (!utils.areStatesEqual(this.state, newState) && this.isShown) { + if (!Utils.areStatesEqual(this.state, newState)) { this.setState(newState); } } @@ -90,8 +81,8 @@ export default class ChannelInviteModal extends React.Component { var data = {}; data.user_id = userId; - client.addChannelMember(ChannelStore.getCurrentId(), data, - function sucess() { + Client.addChannelMember(ChannelStore.getCurrentId(), data, + () => { var nonmembers = this.state.nonmembers; var memberIds = this.state.memberIds; @@ -103,13 +94,12 @@ export default class ChannelInviteModal extends React.Component { } } - this.setState({inviteError: null, memberIds: memberIds, nonmembers: nonmembers}); + this.setState({inviteError: null, memberIds, nonmembers}); AsyncClient.getChannelExtraInfo(true); - }.bind(this), - - function error(err) { + }, + (err) => { this.setState({inviteError: err.message}); - }.bind(this) + } ); } render() { @@ -121,7 +111,7 @@ export default class ChannelInviteModal extends React.Component { var currentMember = ChannelStore.getCurrentMember(); var isAdmin = false; if (currentMember) { - isAdmin = utils.isAdmin(currentMember.roles) || utils.isAdmin(UserStore.getCurrentUser().roles); + isAdmin = Utils.isAdmin(currentMember.roles) || Utils.isAdmin(UserStore.getCurrentUser().roles); } var content; @@ -138,43 +128,32 @@ export default class ChannelInviteModal extends React.Component { } return ( - <div - className='modal fade more-modal' - id='channel_invite' - tabIndex='-1' - role='dialog' - aria-hidden='true' + <Modal + show={this.props.show} + onHide={this.props.onModalDismissed} > - <div - className='modal-dialog' - role='document' - > - <div className='modal-content'> - <div className='modal-header'> - <button - type='button' - className='close' - data-dismiss='modal' - aria-label='Close' - > - <span aria-hidden='true'>×</span> - </button> - <h4 className='modal-title'>Add New Members to <span className='name'>{this.state.channelName}</span></h4> - </div> - <div className='modal-body'> + <Modal.Header closeButton={true}> + <Modal.Title>{'Add New Members to '}<span className='name'>{this.state.channelName}</span></Modal.Title> + </Modal.Header> + <Modal.Body> {inviteError} {content} - </div> - <div className='modal-footer'> + </Modal.Body> + <Modal.Footer> <button type='button' className='btn btn-default' - data-dismiss='modal' - >Close</button> - </div> - </div> - </div> - </div> + onClick={this.props.onModalDismissed} + > + {'Close'} + </button> + </Modal.Footer> + </Modal> ); } } + +ChannelInviteModal.propTypes = { + show: React.PropTypes.bool.isRequired, + onModalDismissed: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/channel_members.jsx b/web/react/components/channel_members.jsx deleted file mode 100644 index d0ea7278b..000000000 --- a/web/react/components/channel_members.jsx +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -const UserStore = require('../stores/user_store.jsx'); -const ChannelStore = require('../stores/channel_store.jsx'); -const AsyncClient = require('../utils/async_client.jsx'); -const MemberList = require('./member_list.jsx'); -const Client = require('../utils/client.jsx'); -const Utils = require('../utils/utils.jsx'); - -export default class ChannelMembers extends React.Component { - constructor(props) { - super(props); - - this.getStateFromStores = this.getStateFromStores.bind(this); - this.onChange = this.onChange.bind(this); - this.handleRemove = this.handleRemove.bind(this); - this.onHide = this.onHide.bind(this); - this.onShow = this.onShow.bind(this); - - this.state = this.getStateFromStores(); - } - getStateFromStores() { - const users = UserStore.getActiveOnlyProfiles(); - let memberList = ChannelStore.getCurrentExtraInfo().members; - - let nonmemberList = []; - for (let id in users) { - if (users.hasOwnProperty(id)) { - let found = false; - for (let i = 0; i < memberList.length; i++) { - if (memberList[i].id === id) { - found = true; - break; - } - } - if (!found) { - nonmemberList.push(users[id]); - } - } - } - - function compareByUsername(a, b) { - if (a.username < b.username) { - return -1; - } else if (a.username > b.username) { - return 1; - } - - return 0; - } - - memberList.sort(compareByUsername); - nonmemberList.sort(compareByUsername); - - const channel = ChannelStore.getCurrent(); - let channelName = ''; - if (channel) { - channelName = channel.display_name; - } - - return { - nonmemberList: nonmemberList, - memberList: memberList, - channelName: channelName - }; - } - onHide() { - this.setState({renderMembers: false}); - } - onShow() { - this.setState({renderMembers: true}); - } - componentDidMount() { - ChannelStore.addExtraInfoChangeListener(this.onChange); - ChannelStore.addChangeListener(this.onChange); - $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.onHide); - - $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow); - } - componentWillUnmount() { - ChannelStore.removeExtraInfoChangeListener(this.onChange); - ChannelStore.removeChangeListener(this.onChange); - } - onChange() { - const newState = this.getStateFromStores(); - if (!Utils.areStatesEqual(this.state, newState)) { - this.setState(newState); - } - } - handleRemove(userId) { - // Make sure the user is a member of the channel - let memberList = this.state.memberList; - let found = false; - for (let i = 0; i < memberList.length; i++) { - if (memberList[i].id === userId) { - found = true; - break; - } - } - - if (!found) { - return; - } - - let data = {}; - data.user_id = userId; - - Client.removeChannelMember(ChannelStore.getCurrentId(), data, - function handleRemoveSuccess() { - let oldMember; - for (let i = 0; i < memberList.length; i++) { - if (userId === memberList[i].id) { - oldMember = memberList[i]; - memberList.splice(i, 1); - break; - } - } - - let nonmemberList = this.state.nonmemberList; - if (oldMember) { - nonmemberList.push(oldMember); - } - - this.setState({memberList: memberList, nonmemberList: nonmemberList}); - AsyncClient.getChannelExtraInfo(true); - }.bind(this), - function handleRemoveError(err) { - this.setState({inviteError: err.message}); - }.bind(this) - ); - } - render() { - const currentMember = ChannelStore.getCurrentMember(); - let isAdmin = false; - if (currentMember) { - isAdmin = Utils.isAdmin(currentMember.roles) || Utils.isAdmin(UserStore.getCurrentUser().roles); - } - - var memberList = null; - if (this.state.renderMembers) { - memberList = ( - <MemberList - memberList={this.state.memberList} - isAdmin={isAdmin} - handleRemove={this.handleRemove} - /> - ); - } - - return ( - <div - className='modal fade more-modal' - ref='modal' - id='channel_members' - tabIndex='-1' - role='dialog' - aria-hidden='true' - > - <div className='modal-dialog'> - <div className='modal-content'> - <div className='modal-header'> - <button - type='button' - className='close' - data-dismiss='modal' - aria-label='Close' - > - <span aria-hidden='true'>×</span> - </button> - <h4 className='modal-title'><span className='name'>{this.state.channelName}</span> Members</h4> - <a - className='btn btn-md btn-primary' - data-toggle='modal' - data-target='#channel_invite' - > - <i className='glyphicon glyphicon-envelope'/> Add New Members - </a> - </div> - <div - ref='modalBody' - className='modal-body' - > - {memberList} - </div> - <div className='modal-footer'> - <button - type='button' - className='btn btn-default' - data-dismiss='modal' - > - Close - </button> - </div> - </div> - </div> - </div> - ); - } -} diff --git a/web/react/components/channel_members_modal.jsx b/web/react/components/channel_members_modal.jsx new file mode 100644 index 000000000..1df0da9cf --- /dev/null +++ b/web/react/components/channel_members_modal.jsx @@ -0,0 +1,193 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +const MemberList = require('./member_list.jsx'); +const ChannelInviteModal = require('./channel_invite_modal.jsx'); + +const UserStore = require('../stores/user_store.jsx'); +const ChannelStore = require('../stores/channel_store.jsx'); + +const AsyncClient = require('../utils/async_client.jsx'); +const Client = require('../utils/client.jsx'); +const Utils = require('../utils/utils.jsx'); + +const Modal = ReactBootstrap.Modal; + +export default class ChannelMembersModal extends React.Component { + constructor(props) { + super(props); + + this.getStateFromStores = this.getStateFromStores.bind(this); + this.onChange = this.onChange.bind(this); + this.handleRemove = this.handleRemove.bind(this); + + const state = this.getStateFromStores(); + state.showInviteModal = false; + this.state = state; + } + getStateFromStores() { + const users = UserStore.getActiveOnlyProfiles(); + const memberList = ChannelStore.getCurrentExtraInfo().members; + + const nonmemberList = []; + for (const id in users) { + if (users.hasOwnProperty(id)) { + let found = false; + for (let i = 0; i < memberList.length; i++) { + if (memberList[i].id === id) { + found = true; + break; + } + } + if (!found) { + nonmemberList.push(users[id]); + } + } + } + + function compareByUsername(a, b) { + if (a.username < b.username) { + return -1; + } else if (a.username > b.username) { + return 1; + } + + return 0; + } + + memberList.sort(compareByUsername); + nonmemberList.sort(compareByUsername); + + const channel = ChannelStore.getCurrent(); + let channelName = ''; + if (channel) { + channelName = channel.display_name; + } + + return { + nonmemberList, + memberList, + channelName + }; + } + componentWillReceiveProps(nextProps) { + if (!this.props.show && nextProps.show) { + ChannelStore.addExtraInfoChangeListener(this.onChange); + ChannelStore.addChangeListener(this.onChange); + } else if (this.props.show && !nextProps.show) { + ChannelStore.removeExtraInfoChangeListener(this.onChange); + ChannelStore.removeChangeListener(this.onChange); + } + } + onChange() { + const newState = this.getStateFromStores(); + if (!Utils.areStatesEqual(this.state, newState)) { + this.setState(newState); + } + } + handleRemove(userId) { + // Make sure the user is a member of the channel + const memberList = this.state.memberList; + let found = false; + for (let i = 0; i < memberList.length; i++) { + if (memberList[i].id === userId) { + found = true; + break; + } + } + + if (!found) { + return; + } + + const data = {}; + data.user_id = userId; + + Client.removeChannelMember(ChannelStore.getCurrentId(), data, + () => { + let oldMember; + for (let i = 0; i < memberList.length; i++) { + if (userId === memberList[i].id) { + oldMember = memberList[i]; + memberList.splice(i, 1); + break; + } + } + + const nonmemberList = this.state.nonmemberList; + if (oldMember) { + nonmemberList.push(oldMember); + } + + this.setState({memberList, nonmemberList}); + AsyncClient.getChannelExtraInfo(true); + }, + (err) => { + this.setState({inviteError: err.message}); + } + ); + } + render() { + const currentMember = ChannelStore.getCurrentMember(); + let isAdmin = false; + if (currentMember) { + isAdmin = Utils.isAdmin(currentMember.roles) || Utils.isAdmin(UserStore.getCurrentUser().roles); + } + + return ( + <div> + <Modal + show={this.props.show} + onHide={this.props.onModalDismissed} + > + <Modal.Header closeButton={true}> + <Modal.Title><span className='name'>{this.state.channelName}</span>{' Members'}</Modal.Title> + <a + className='btn btn-md btn-primary' + href='#' + onClick={() => { + this.setState({showInviteModal: true}); + this.props.onModalDismissed(); + }} + > + <i className='glyphicon glyphicon-envelope'/>{' Add New Members'} + </a> + </Modal.Header> + <Modal.Body> + <div className='col-sm-12'> + <div className='team-member-list'> + <MemberList + memberList={this.state.memberList} + isAdmin={isAdmin} + handleRemove={this.handleRemove} + /> + </div> + </div> + </Modal.Body> + <Modal.Footer> + <button + type='button' + className='btn btn-default' + onClick={this.props.onModalDismissed} + > + {'Close'} + </button> + </Modal.Footer> + </Modal> + <ChannelInviteModal + show={this.state.showInviteModal} + onModalDismissed={() => this.setState({showInviteModal: false})} + /> + </div> + ); + } +} + +ChannelMembersModal.defaultProps = { + show: false +}; + +ChannelMembersModal.propTypes = { + show: React.PropTypes.bool.isRequired, + onModalDismissed: React.PropTypes.func.isRequired +}; diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx index 5b3c74e82..2557a55ca 100644 --- a/web/react/components/edit_channel_modal.jsx +++ b/web/react/components/edit_channel_modal.jsx @@ -12,6 +12,7 @@ export default class EditChannelModal extends React.Component { this.handleUserInput = this.handleUserInput.bind(this); this.handleClose = this.handleClose.bind(this); this.onShow = this.onShow.bind(this); + this.handleShown = this.handleShown.bind(this); this.state = { header: '', @@ -55,9 +56,13 @@ export default class EditChannelModal extends React.Component { const button = e.relatedTarget; this.setState({header: $(button).attr('data-header'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''}); } + handleShown() { + $('#edit_channel #edit_header').focus(); + } componentDidMount() { $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow); $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose); + $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', this.handleShown); } componentWillUnmount() { $(ReactDOM.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose); @@ -114,6 +119,7 @@ export default class EditChannelModal extends React.Component { <textarea className='form-control no-resize' rows='6' + id='edit_header' maxLength='1024' value={this.state.header} onChange={this.handleUserInput} diff --git a/web/react/components/edit_channel_purpose_modal.jsx b/web/react/components/edit_channel_purpose_modal.jsx index 4d162cfe7..4cb96a3ff 100644 --- a/web/react/components/edit_channel_purpose_modal.jsx +++ b/web/react/components/edit_channel_purpose_modal.jsx @@ -15,6 +15,12 @@ export default class EditChannelPurposeModal extends React.Component { this.state = {serverError: ''}; } + componentDidUpdate() { + if (this.props.show) { + $(ReactDOM.findDOMNode(this.refs.purpose)).focus(); + } + } + handleHide() { this.setState({serverError: ''}); @@ -77,6 +83,7 @@ export default class EditChannelPurposeModal extends React.Component { return ( <Modal className='modal-edit-channel-purpose' + ref='modal' show={this.props.show} onHide={this.handleHide} > diff --git a/web/react/components/member_list.jsx b/web/react/components/member_list.jsx index 70eb0a500..9c0243291 100644 --- a/web/react/components/member_list.jsx +++ b/web/react/components/member_list.jsx @@ -17,26 +17,26 @@ export default class MemberList extends React.Component { var message = ''; if (members.length === 0) { - message = <span>No users to add.</span>; + message = <tr><td>No users to add.</td></tr>; } return ( <table className='table more-table member-list-holder'> <tbody> - {members.map(function mymembers(member) { - return ( - <MemberListItem - key={member.id} - member={member} - isAdmin={this.props.isAdmin} - handleInvite={this.props.handleInvite} - handleRemove={this.props.handleRemove} - handleMakeAdmin={this.props.handleMakeAdmin} - /> - ); - }, this)} + {members.map(function mymembers(member) { + return ( + <MemberListItem + key={member.id} + member={member} + isAdmin={this.props.isAdmin} + handleInvite={this.props.handleInvite} + handleRemove={this.props.handleRemove} + handleMakeAdmin={this.props.handleMakeAdmin} + /> + ); + }, this)} + {message} </tbody> - {message} </table> ); } diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx index f7778f25f..ff53816c7 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -1,22 +1,26 @@ // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -var Client = require('../utils/client.jsx'); -var AsyncClient = require('../utils/async_client.jsx'); -var UserStore = require('../stores/user_store.jsx'); -var ChannelStore = require('../stores/channel_store.jsx'); -var TeamStore = require('../stores/team_store.jsx'); -var MessageWrapper = require('./message_wrapper.jsx'); -var NotifyCounts = require('./notify_counts.jsx'); const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx'); +const MessageWrapper = require('./message_wrapper.jsx'); +const NotifyCounts = require('./notify_counts.jsx'); +const ChannelMembersModal = require('./channel_members_modal.jsx'); +const ChannelInviteModal = require('./channel_invite_modal.jsx'); + +const UserStore = require('../stores/user_store.jsx'); +const ChannelStore = require('../stores/channel_store.jsx'); +const TeamStore = require('../stores/team_store.jsx'); + +const Client = require('../utils/client.jsx'); +const AsyncClient = require('../utils/async_client.jsx'); const Utils = require('../utils/utils.jsx'); -var Constants = require('../utils/constants.jsx'); -var ActionTypes = Constants.ActionTypes; -var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); +const Constants = require('../utils/constants.jsx'); +const ActionTypes = Constants.ActionTypes; +const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); -var Popover = ReactBootstrap.Popover; -var OverlayTrigger = ReactBootstrap.OverlayTrigger; +const Popover = ReactBootstrap.Popover; +const OverlayTrigger = ReactBootstrap.OverlayTrigger; export default class Navbar extends React.Component { constructor(props) { @@ -29,6 +33,8 @@ export default class Navbar extends React.Component { const state = this.getStateFromStores(); state.showEditChannelPurposeModal = false; + state.showMembersModal = false; + state.showInviteModal = false; this.state = state; } getStateFromStores() { @@ -45,17 +51,18 @@ export default class Navbar extends React.Component { } componentWillUnmount() { ChannelStore.removeChangeListener(this.onChange); + ChannelStore.removeExtraInfoChangeListener(this.onChange); } handleSubmit(e) { e.preventDefault(); } handleLeave() { Client.leaveChannel(this.state.channel.id, - function success() { + () => { AsyncClient.getChannels(true); window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square'; }, - function error(err) { + (err) => { AsyncClient.dispatchError(err, 'handleLeave'); } ); @@ -104,7 +111,7 @@ export default class Navbar extends React.Component { data-channelid={channel.id} href='#' > - View Info + {'View Info'} </a> </li> ); @@ -120,7 +127,7 @@ export default class Navbar extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - Set Channel Header... + {'Set Channel Header...'} </a> </li> ); @@ -145,11 +152,10 @@ export default class Navbar extends React.Component { <li role='presentation'> <a role='menuitem' - data-toggle='modal' - data-target='#channel_invite' href='#' + onClick={() => this.setState({showInviteModal: false})} > - Add Members + {'Add Members'} </a> </li> ); @@ -161,7 +167,7 @@ export default class Navbar extends React.Component { href='#' onClick={this.handleLeave} > - Leave Channel + {'Leave Channel'} </a> </li> ); @@ -175,11 +181,10 @@ export default class Navbar extends React.Component { <li role='presentation'> <a role='menuitem' - data-toggle='modal' - data-target='#channel_members' href='#' + onClick={() => this.setState({showMembersModal: true})} > - Manage Members + {'Manage Members'} </a> </li> ); @@ -195,7 +200,7 @@ export default class Navbar extends React.Component { data-name={channel.name} data-channelid={channel.id} > - Rename Channel... + {'Rename Channel...'} </a> </li> ); @@ -210,7 +215,7 @@ export default class Navbar extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - Delete Channel... + {'Delete Channel...'} </a> </li> ); @@ -228,7 +233,7 @@ export default class Navbar extends React.Component { data-title={channel.display_name} data-channelid={channel.id} > - Notification Preferences + {'Notification Preferences'} </a> </li> ); @@ -299,7 +304,7 @@ export default class Navbar extends React.Component { data-toggle='collapse' data-target='#navbar-collapse-1' > - <span className='sr-only'>Toggle sidebar</span> + <span className='sr-only'>{'Toggle sidebar'}</span> <span className='icon-bar'></span> <span className='icon-bar'></span> <span className='icon-bar'></span> @@ -315,7 +320,7 @@ export default class Navbar extends React.Component { data-target='#sidebar-nav' onClick={this.toggleLeftSidebar} > - <span className='sr-only'>Toggle sidebar</span> + <span className='sr-only'>{'Toggle sidebar'}</span> <span className='icon-bar'></span> <span className='icon-bar'></span> <span className='icon-bar'></span> @@ -426,6 +431,14 @@ export default class Navbar extends React.Component { onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})} channel={channel} /> + <ChannelMembersModal + show={this.state.showMembersModal} + onModalDismissed={() => this.setState({showMembersModal: false})} + /> + <ChannelInviteModal + show={this.state.showInviteModal} + onModalDismissed={() => this.setState({showInviteModal: false})} + /> </div> ); } diff --git a/web/react/components/rename_channel_modal.jsx b/web/react/components/rename_channel_modal.jsx index 80f0956f2..9fb3af035 100644 --- a/web/react/components/rename_channel_modal.jsx +++ b/web/react/components/rename_channel_modal.jsx @@ -16,6 +16,7 @@ export default class RenameChannelModal extends React.Component { this.displayNameKeyUp = this.displayNameKeyUp.bind(this); this.handleClose = this.handleClose.bind(this); this.handleShow = this.handleShow.bind(this); + this.handleShown = this.handleShown.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.state = { @@ -118,9 +119,13 @@ export default class RenameChannelModal extends React.Component { const button = $(e.relatedTarget); this.setState({displayName: button.attr('data-display'), channelName: button.attr('data-name'), channelId: button.attr('data-channelid')}); } + handleShown() { + $('#rename_channel #display_name').focus(); + } componentDidMount() { $(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.handleShow); $(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose); + $(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', this.handleShown); } componentWillUnmount() { $(ReactDOM.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose); @@ -176,6 +181,7 @@ export default class RenameChannelModal extends React.Component { onChange={this.onDisplayNameChange} type='text' ref='displayName' + id='display_name' className='form-control' placeholder='Enter display name' value={this.state.displayName} diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index aab9919a4..8b5f7a381 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -20,6 +20,7 @@ const Utils = require('../utils/utils.jsx'); const Constants = require('../utils/constants.jsx'); const Preferences = Constants.Preferences; const TutorialSteps = Constants.TutorialSteps; +const NotificationPrefs = Constants.NotificationPrefs; const Tooltip = ReactBootstrap.Tooltip; const OverlayTrigger = ReactBootstrap.OverlayTrigger; @@ -76,6 +77,8 @@ export default class Sidebar extends React.Component { if (ch.type === 'D') { chMentionCount = chUnreadCount; chUnreadCount = 0; + } else if (chMember.notify_props && chMember.notify_props.mark_unread === NotificationPrefs.MENTION) { + chUnreadCount = 0; } channelUnreadCounts[ch.id] = {msgs: chUnreadCount, mentions: chMentionCount}; @@ -362,7 +365,7 @@ export default class Sidebar extends React.Component { var unread = false; if (channelMember) { msgCount = unreadCount.msgs + unreadCount.mentions; - unread = (msgCount > 0 && channelMember.notify_props.mark_unread !== 'mention') || channelMember.mention_count > 0; + unread = msgCount > 0 || channelMember.mention_count > 0; } if (unread) { diff --git a/web/react/components/tutorial/tutorial_tip.jsx b/web/react/components/tutorial/tutorial_tip.jsx index c85acb346..3094b2f4c 100644 --- a/web/react/components/tutorial/tutorial_tip.jsx +++ b/web/react/components/tutorial/tutorial_tip.jsx @@ -98,7 +98,7 @@ export default class TutorialTip extends React.Component { <div className='tutorial__circles'>{dots}</div> <div className='text-right'> <button - className='btn btn-default' + className='btn btn-primary' onClick={this.handleNext} > {buttonText} diff --git a/web/react/components/user_settings/code_theme_chooser.jsx b/web/react/components/user_settings/code_theme_chooser.jsx deleted file mode 100644 index eef4b24ba..000000000 --- a/web/react/components/user_settings/code_theme_chooser.jsx +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -var Constants = require('../../utils/constants.jsx'); - -export default class CodeThemeChooser extends React.Component { - constructor(props) { - super(props); - this.state = {}; - } - render() { - const theme = this.props.theme; - - const premadeThemes = []; - for (const k in Constants.CODE_THEMES) { - if (Constants.CODE_THEMES.hasOwnProperty(k)) { - let activeClass = ''; - if (k === theme.codeTheme) { - activeClass = 'active'; - } - - premadeThemes.push( - <div - className='col-xs-6 col-sm-3 premade-themes' - key={'premade-theme-key' + k} - > - <div - className={activeClass} - onClick={() => this.props.updateTheme(k)} - > - <label> - <img - className='img-responsive' - src={'/static/images/themes/code_themes/' + k + '.png'} - /> - <div className='theme-label'>{Constants.CODE_THEMES[k]}</div> - </label> - </div> - </div> - ); - } - } - - return ( - <div className='row'> - {premadeThemes} - </div> - ); - } -} - -CodeThemeChooser.propTypes = { - theme: React.PropTypes.object.isRequired, - updateTheme: React.PropTypes.func.isRequired -}; diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx index 095e5b622..895d0c500 100644 --- a/web/react/components/user_settings/custom_theme_chooser.jsx +++ b/web/react/components/user_settings/custom_theme_chooser.jsx @@ -55,28 +55,70 @@ export default class CustomThemeChooser extends React.Component { const elements = []; let colors = ''; Constants.THEME_ELEMENTS.forEach((element, index) => { - elements.push( - <div - className='col-sm-4 form-group' - key={'custom-theme-key' + index} - > - <label className='custom-label'>{element.uiName}</label> + if (element.id === 'codeTheme') { + const codeThemeOptions = []; + + element.themes.forEach((codeTheme, codeThemeIndex) => { + codeThemeOptions.push( + <option + key={'code-theme-key' + codeThemeIndex} + value={codeTheme.id} + > + {codeTheme.uiName} + </option> + ); + }); + + elements.push( <div - className='input-group color-picker' - id={element.id} + className='col-sm-4 form-group' + key={'custom-theme-key' + index} > - <input - className='form-control' - type='text' - defaultValue={theme[element.id]} - onChange={this.onInputChange} - /> - <span className='input-group-addon'><i></i></span> + <label className='custom-label'>{element.uiName}</label> + <div + className='input-group theme-group dropdown' + id={element.id} + > + <select + className='form-control' + type='text' + defaultValue={theme[element.id]} + onChange={this.onInputChange} + > + {codeThemeOptions} + </select> + <span className='input-group-addon'> + <img + src={'/static/images/themes/code_themes/' + theme[element.id] + '.png'} + /> + </span> + </div> </div> - </div> - ); + ); + } else { + elements.push( + <div + className='col-sm-4 form-group' + key={'custom-theme-key' + index} + > + <label className='custom-label'>{element.uiName}</label> + <div + className='input-group color-picker' + id={element.id} + > + <input + className='form-control' + type='text' + defaultValue={theme[element.id]} + onChange={this.onInputChange} + /> + <span className='input-group-addon'><i></i></span> + </div> + </div> + ); - colors += theme[element.id] + ','; + colors += theme[element.id] + ','; + } }); colors += theme.codeTheme; @@ -87,6 +129,7 @@ export default class CustomThemeChooser extends React.Component { {'Copy and paste to share theme colors:'} </label> <input + readOnly='true' type='text' className='form-control' value={colors} diff --git a/web/react/components/user_settings/user_settings_appearance.jsx b/web/react/components/user_settings/user_settings_appearance.jsx index 425645c1f..d73b5f476 100644 --- a/web/react/components/user_settings/user_settings_appearance.jsx +++ b/web/react/components/user_settings/user_settings_appearance.jsx @@ -7,7 +7,6 @@ var Utils = require('../../utils/utils.jsx'); const CustomThemeChooser = require('./custom_theme_chooser.jsx'); const PremadeThemeChooser = require('./premade_theme_chooser.jsx'); -const CodeThemeChooser = require('./code_theme_chooser.jsx'); const AppDispatcher = require('../../dispatcher/app_dispatcher.jsx'); const Constants = require('../../utils/constants.jsx'); const ActionTypes = Constants.ActionTypes; @@ -19,7 +18,6 @@ export default class UserSettingsAppearance extends React.Component { this.onChange = this.onChange.bind(this); this.submitTheme = this.submitTheme.bind(this); this.updateTheme = this.updateTheme.bind(this); - this.updateCodeTheme = this.updateCodeTheme.bind(this); this.deactivate = this.deactivate.bind(this); this.resetFields = this.resetFields.bind(this); this.handleImportModal = this.handleImportModal.bind(this); @@ -100,10 +98,6 @@ export default class UserSettingsAppearance extends React.Component { ); } updateTheme(theme) { - if (!theme.codeTheme) { - theme.codeTheme = this.state.theme.codeTheme; - } - let themeChanged = this.state.theme.length === theme.length; if (!themeChanged) { for (const field in theme) { @@ -121,11 +115,6 @@ export default class UserSettingsAppearance extends React.Component { this.setState({theme}); Utils.applyTheme(theme); } - updateCodeTheme(codeTheme) { - var theme = this.state.theme; - theme.codeTheme = codeTheme; - this.updateTheme(theme); - } updateType(type) { this.setState({type}); } @@ -203,12 +192,6 @@ export default class UserSettingsAppearance extends React.Component { </div> {custom} <hr /> - <strong className='radio'>{'Code Theme'}</strong> - <CodeThemeChooser - theme={this.state.theme} - updateTheme={this.updateCodeTheme} - /> - <hr /> {serverError} <a className='btn btn-sm btn-primary' diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index 2f872effd..8781d52a5 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -18,8 +18,6 @@ var MoreChannelsModal = require('../components/more_channels.jsx'); var PostDeletedModal = require('../components/post_deleted_modal.jsx'); var ChannelNotificationsModal = require('../components/channel_notifications.jsx'); var TeamSettingsModal = require('../components/team_settings_modal.jsx'); -var ChannelMembersModal = require('../components/channel_members.jsx'); -var ChannelInviteModal = require('../components/channel_invite_modal.jsx'); var TeamMembersModal = require('../components/team_members.jsx'); var ChannelInfoModal = require('../components/channel_info_modal.jsx'); var RemovedFromChannelModal = require('../components/removed_from_channel_modal.jsx'); @@ -120,16 +118,6 @@ function setupChannelPage(props) { ); ReactDOM.render( - <ChannelMembersModal />, - document.getElementById('channel_members_modal') - ); - - ReactDOM.render( - <ChannelInviteModal />, - document.getElementById('channel_invite_modal') - ); - - ReactDOM.render( <ChannelInfoModal />, document.getElementById('channel_info_modal') ); diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index b8d346ba7..58ee8e2d2 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -159,7 +159,8 @@ module.exports = { buttonBg: '#2389d7', buttonColor: '#FFFFFF', mentionHighlightBg: '#fff2bb', - mentionHighlightLink: '#2f81b7' + mentionHighlightLink: '#2f81b7', + codeTheme: 'github' }, organization: { type: 'Organization', @@ -181,7 +182,8 @@ module.exports = { buttonBg: '#1dacfc', buttonColor: '#FFFFFF', mentionHighlightBg: '#fff2bb', - mentionHighlightLink: '#2f81b7' + mentionHighlightLink: '#2f81b7', + codeTheme: 'github' }, mattermostDark: { type: 'Mattermost Dark', @@ -203,7 +205,8 @@ module.exports = { buttonBg: '#4CBBA4', buttonColor: '#FFFFFF', mentionHighlightBg: '#984063', - mentionHighlightLink: '#A4FFEB' + mentionHighlightLink: '#A4FFEB', + codeTheme: 'solarized_dark' }, windows10: { type: 'Windows Dark', @@ -225,7 +228,8 @@ module.exports = { buttonBg: '#0177e7', buttonColor: '#FFFFFF', mentionHighlightBg: '#784098', - mentionHighlightLink: '#A4FFEB' + mentionHighlightLink: '#A4FFEB', + codeTheme: 'monokai' } }, THEME_ELEMENTS: [ @@ -304,14 +308,30 @@ module.exports = { { id: 'mentionHighlightLink', uiName: 'Mention Highlight Link' + }, + { + id: 'codeTheme', + uiName: 'Code Theme', + themes: [ + { + id: 'solarized_dark', + uiName: 'Solarized Dark' + }, + { + id: 'solarized_light', + uiName: 'Solarized Light' + }, + { + id: 'github', + uiName: 'GitHub' + }, + { + id: 'monokai', + uiName: 'Monokai' + } + ] } ], - CODE_THEMES: { - github: 'GitHub', - solarized_light: 'Solarized light', - monokai: 'Monokai', - solarized_dark: 'Solarized Dark' - }, DEFAULT_CODE_THEME: 'github', Preferences: { CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show', @@ -364,5 +384,8 @@ module.exports = { BOTTOM: 1, POST: 2, SIDEBAR_OPEN: 3 + }, + NotificationPrefs: { + MENTION: 'mention' } }; diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx index e8d34dccd..575b6d011 100644 --- a/web/react/utils/utils.jsx +++ b/web/react/utils/utils.jsx @@ -438,11 +438,6 @@ export function toTitleCase(str) { } export function applyTheme(theme) { - if (!theme.codeTheme) { - theme.codeTheme = Constants.DEFAULT_CODE_THEME; - } - updateCodeTheme(theme.codeTheme); - if (theme.sidebarBg) { changeCss('.sidebar--left, .settings-modal .settings-table .settings-links, .sidebar--menu', 'background:' + theme.sidebarBg, 1); } @@ -533,7 +528,7 @@ export function applyTheme(theme) { changeCss('.dropdown-menu, .popover ', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 3); changeCss('.dropdown-menu, .popover ', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 2); changeCss('.dropdown-menu, .popover ', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px', 1); - changeCss('.post-body hr, .loading-screen .loading__content .round, .tutorial__circles .circle, .tip-overlay .tutorial__circles .circle.active', 'background:' + theme.centerChannelColor, 1); + changeCss('.post-body hr, .loading-screen .loading__content .round, .tutorial__circles .circle', 'background:' + theme.centerChannelColor, 1); changeCss('.channel-header .heading', 'color:' + theme.centerChannelColor, 1); changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1); changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1); @@ -598,6 +593,11 @@ export function applyTheme(theme) { if (theme.mentionHighlightLink) { changeCss('.mention-highlight .mention-link', 'color:' + theme.mentionHighlightLink, 1); } + + if (!theme.codeTheme) { + theme.codeTheme = Constants.DEFAULT_CODE_THEME; + } + updateCodeTheme(theme.codeTheme); } export function changeCss(className, classValue, classRepeat) { // we need invisible container to store additional css definitions diff --git a/web/sass-files/sass/partials/_access-history.scss b/web/sass-files/sass/partials/_access-history.scss index a3289ecc0..c8a0b28bd 100644 --- a/web/sass-files/sass/partials/_access-history.scss +++ b/web/sass-files/sass/partials/_access-history.scss @@ -19,10 +19,6 @@ border-bottom: 1px solid #ddd; padding-bottom: 15px; } - .report__time { - font-weight: 600; - font-size: 15px; - } .report__info { @include opacity(0.8); } diff --git a/web/sass-files/sass/partials/_activity-log.scss b/web/sass-files/sass/partials/_activity-log.scss index 2fb37a3bb..f61c35a28 100644 --- a/web/sass-files/sass/partials/_activity-log.scss +++ b/web/sass-files/sass/partials/_activity-log.scss @@ -36,7 +36,7 @@ text-align: right; } .report__platform { - font-size: 16px; + font-size: 15px; font-weight: 600; .fa { margin-right: 6px; @@ -47,5 +47,5 @@ } } .session-help-text { - padding: 20px 20px 5px 20px; + padding: 0 0 20px; }
\ No newline at end of file diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss index 2830026c9..ad4a65c00 100644 --- a/web/sass-files/sass/partials/_base.scss +++ b/web/sass-files/sass/partials/_base.scss @@ -31,7 +31,7 @@ body { } .container-fluid { - @include clearfix; + @include legacy-pie-clearfix; height: 100%; position: relative; } diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss index 0333e0c65..852d19a29 100644 --- a/web/sass-files/sass/partials/_modal.scss +++ b/web/sass-files/sass/partials/_modal.scss @@ -368,7 +368,7 @@ } .modal-body { - padding: 10px 0 0; + padding: 10px 0 20px; @include clearfix; } diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss index 3be83ed2f..b304450bc 100644 --- a/web/sass-files/sass/partials/_settings.scss +++ b/web/sass-files/sass/partials/_settings.scss @@ -125,6 +125,15 @@ } .appearance-section { + .theme-group { + .input-group-addon { + padding: 4px 5px; + width: 40px; + img { + border: 1px solid rgba(black, 0.15); + } + } + } .premade-themes { margin-bottom: 10px; .theme-label { diff --git a/web/sass-files/sass/partials/_tutorial.scss b/web/sass-files/sass/partials/_tutorial.scss index 70216aa97..c1bf5fd59 100644 --- a/web/sass-files/sass/partials/_tutorial.scss +++ b/web/sass-files/sass/partials/_tutorial.scss @@ -107,11 +107,6 @@ width: 7px; height: 7px; margin: 0 4px; - &.active { - background: #000; - @include opacity(0.4); - } - } } @@ -153,10 +148,17 @@ padding-bottom: 100px; padding: 20px 40px 40px; .tutorial__steps { + position: relative; max-width: 310px; - min-height: 420px; + min-height: 350px; + margin-bottom: 50px; text-align: left; display: inline-block; + padding-bottom: 30px; + } + .btn-primary { + position: absolute; + bottom: 0; } } h1 { diff --git a/web/static/images/themes/code_themes/github.png b/web/static/images/themes/code_themes/github.png Binary files differindex d0538d6c0..a49b877b1 100644 --- a/web/static/images/themes/code_themes/github.png +++ b/web/static/images/themes/code_themes/github.png diff --git a/web/static/images/themes/code_themes/monokai.png b/web/static/images/themes/code_themes/monokai.png Binary files differindex 8f92d2a18..a71225b68 100644 --- a/web/static/images/themes/code_themes/monokai.png +++ b/web/static/images/themes/code_themes/monokai.png diff --git a/web/static/images/themes/code_themes/solarized_dark.png b/web/static/images/themes/code_themes/solarized_dark.png Binary files differindex 76055c678..07774ff20 100644 --- a/web/static/images/themes/code_themes/solarized_dark.png +++ b/web/static/images/themes/code_themes/solarized_dark.png diff --git a/web/static/images/themes/code_themes/solarized_light.png b/web/static/images/themes/code_themes/solarized_light.png Binary files differindex b9595c22d..fc71dbb87 100644 --- a/web/static/images/themes/code_themes/solarized_light.png +++ b/web/static/images/themes/code_themes/solarized_light.png diff --git a/web/templates/channel.html b/web/templates/channel.html index aabd01ecb..c15cea178 100644 --- a/web/templates/channel.html +++ b/web/templates/channel.html @@ -25,8 +25,6 @@ <div id="new_channel_modal"></div> <div id="post_deleted_modal"></div> <div id="channel_notifications_modal"></div> - <div id="channel_members_modal"></div> - <div id="channel_invite_modal"></div> <div id="team_members_modal"></div> <div id="direct_channel_modal"></div> <div id="channel_info_modal"></div> diff --git a/web/templates/head.html b/web/templates/head.html index 24f9862c0..a73e809a7 100644 --- a/web/templates/head.html +++ b/web/templates/head.html @@ -85,7 +85,7 @@ !function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","group","track","ready","alias","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)}analytics.load=function(t){var e=document.createElement("script");e.type="text/javascript";e.async=!0;e.src=("https:"===document.location.protocol?"https://":"http://")+"cdn.segment.com/analytics.js/v1/"+t+"/analytics.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(e,n)};analytics.SNIPPET_VERSION="3.0.1"; analytics.load(window.mm_config.SegmentDeveloperKey); if (window.mm_user) { - analytics.identify(user.id, { + analytics.identify(window.mm_user.id, { name: window.mm_user.nickname, email: window.mm_user.email, createdAt: window.mm_user.create_at, |