diff options
-rw-r--r-- | api/channel.go | 7 | ||||
-rw-r--r-- | api/channel_test.go | 4 | ||||
-rw-r--r-- | doc/developer/tests/test-links.md | 16 | ||||
-rw-r--r-- | model/channel_extra.go | 5 | ||||
-rw-r--r-- | store/sql_channel_store.go | 20 | ||||
-rw-r--r-- | store/sql_channel_store_test.go | 8 | ||||
-rw-r--r-- | store/store.go | 1 | ||||
-rw-r--r-- | web/react/components/channel_header.jsx | 6 | ||||
-rw-r--r-- | web/react/components/invite_member_modal.jsx | 44 | ||||
-rw-r--r-- | web/react/components/popover_list_members.jsx | 4 | ||||
-rw-r--r-- | web/react/components/posts_view.jsx | 8 | ||||
-rw-r--r-- | web/react/components/sidebar.jsx | 2 | ||||
-rw-r--r-- | web/react/components/textbox.jsx | 7 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_files.scss | 1 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_navbar.scss | 6 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_post.scss | 37 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_post_right.scss | 16 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_responsive.scss | 15 | ||||
-rw-r--r-- | web/sass-files/sass/partials/_settings.scss | 4 |
19 files changed, 151 insertions, 60 deletions
diff --git a/api/channel.go b/api/channel.go index 44be1cf97..75ca9680d 100644 --- a/api/channel.go +++ b/api/channel.go @@ -707,6 +707,7 @@ func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) { scm := Srv.Store.Channel().GetMember(id, c.Session.UserId) ecm := Srv.Store.Channel().GetExtraMembers(id, 20) + ccm := Srv.Store.Channel().GetMemberCount(id) if cmresult := <-scm; cmresult.Err != nil { c.Err = cmresult.Err @@ -714,9 +715,13 @@ func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) { } else if ecmresult := <-ecm; ecmresult.Err != nil { c.Err = ecmresult.Err return + } else if ccmresult := <-ccm; ccmresult.Err != nil { + c.Err = ccmresult.Err + return } else { member := cmresult.Data.(model.ChannelMember) extraMembers := ecmresult.Data.([]model.ExtraMember) + memberCount := ccmresult.Data.(int64) if !c.HasPermissionsToTeam(channel.TeamId, "getChannelExtraInfo") { return @@ -732,7 +737,7 @@ func getChannelExtraInfo(c *Context, w http.ResponseWriter, r *http.Request) { return } - data := model.ChannelExtra{Id: channel.Id, Members: extraMembers} + data := model.ChannelExtra{Id: channel.Id, Members: extraMembers, MemberCount: memberCount} w.Header().Set(model.HEADER_ETAG_SERVER, extraEtag) w.Header().Set("Expires", "-1") w.Write([]byte(data.ToJson())) diff --git a/api/channel_test.go b/api/channel_test.go index a41f63b1b..faed387dd 100644 --- a/api/channel_test.go +++ b/api/channel_test.go @@ -677,6 +677,10 @@ func TestGetChannelExtraInfo(t *testing.T) { data := rget.Data.(*model.ChannelExtra) if data.Id != channel1.Id { t.Fatal("couldnt't get extra info") + } else if len(data.Members) != 1 { + t.Fatal("got incorrect members") + } else if data.MemberCount != 1 { + t.Fatal("got incorrect member count") } // diff --git a/doc/developer/tests/test-links.md b/doc/developer/tests/test-links.md new file mode 100644 index 000000000..62b729b30 --- /dev/null +++ b/doc/developer/tests/test-links.md @@ -0,0 +1,16 @@ + +# Link Testing + +Links in Mattermosts should render as specified below. Paste the below text into Mattermost to test text processing. + +``` +These strings should auto-link: + +http://wikipedia.com +https://wikipedia.com +www.wikipedia.com + +These strings should not auto-link: + +Readme.md +``` diff --git a/model/channel_extra.go b/model/channel_extra.go index c6f0ca192..55da588af 100644 --- a/model/channel_extra.go +++ b/model/channel_extra.go @@ -23,8 +23,9 @@ func (o *ExtraMember) Sanitize(options map[string]bool) { } type ChannelExtra struct { - Id string `json:"id"` - Members []ExtraMember `json:"members"` + Id string `json:"id"` + Members []ExtraMember `json:"members"` + MemberCount int64 `json:"member_count"` } func (o *ChannelExtra) ToJson() string { diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go index fc4e19442..a9f99bd67 100644 --- a/store/sql_channel_store.go +++ b/store/sql_channel_store.go @@ -542,6 +542,26 @@ func (s SqlChannelStore) GetMember(channelId string, userId string) StoreChannel return storeChannel } +func (s SqlChannelStore) GetMemberCount(channelId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + count, err := s.GetReplica().SelectInt("SELECT count(*) FROM ChannelMembers WHERE ChannelId = :ChannelId", map[string]interface{}{"ChannelId": channelId}) + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.GetMemberCount", "We couldn't get the channel member count", "channel_id="+channelId+", "+err.Error()) + } else { + result.Data = count + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + func (s SqlChannelStore) GetExtraMembers(channelId string, limit int) StoreChannel { storeChannel := make(StoreChannel) diff --git a/store/sql_channel_store_test.go b/store/sql_channel_store_test.go index f6a0fb713..8662fcbd3 100644 --- a/store/sql_channel_store_test.go +++ b/store/sql_channel_store_test.go @@ -339,15 +339,15 @@ func TestChannelMemberStore(t *testing.T) { t.Fatal("Member update time incorrect") } - members := (<-store.Channel().GetMembers(o1.ChannelId)).Data.([]model.ChannelMember) - if len(members) != 2 { + count := (<-store.Channel().GetMemberCount(o1.ChannelId)).Data.(int64) + if count != 2 { t.Fatal("should have saved 2 members") } Must(store.Channel().RemoveMember(o2.ChannelId, o2.UserId)) - members = (<-store.Channel().GetMembers(o1.ChannelId)).Data.([]model.ChannelMember) - if len(members) != 1 { + count = (<-store.Channel().GetMemberCount(o1.ChannelId)).Data.(int64) + if count != 1 { t.Fatal("should have removed 1 member") } diff --git a/store/store.go b/store/store.go index ce4d90883..13b59b582 100644 --- a/store/store.go +++ b/store/store.go @@ -70,6 +70,7 @@ type ChannelStore interface { UpdateMember(member *model.ChannelMember) StoreChannel GetMembers(channelId string) StoreChannel GetMember(channelId string, userId string) StoreChannel + GetMemberCount(channelId string) StoreChannel RemoveMember(channelId string, userId string) StoreChannel GetExtraMembers(channelId string, limit int) StoreChannel CheckPermissionsTo(teamId string, channelId string, userId string) StoreChannel diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 52802588b..a8d4ec100 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -39,11 +39,14 @@ export default class ChannelHeader extends React.Component { this.state = state; } getStateFromStores() { + const extraInfo = ChannelStore.getCurrentExtraInfo(); + return { channel: ChannelStore.getCurrent(), memberChannel: ChannelStore.getCurrentMember(), memberTeam: UserStore.getCurrentUser(), - users: ChannelStore.getCurrentExtraInfo().members, + users: extraInfo.members, + userCount: extraInfo.member_count, searchVisible: SearchStore.getSearchResults() !== null }; } @@ -373,6 +376,7 @@ export default class ChannelHeader extends React.Component { <th> <PopoverListMembers members={this.state.users} + memberCount={this.state.userCount} channelId={channel.id} /> </th> diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index c09477a69..3f6ad3358 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -31,7 +31,8 @@ export default class InviteMemberModal extends React.Component { firstNameErrors: {}, lastNameErrors: {}, emailEnabled: global.window.mm_config.SendEmailNotifications === 'true', - showConfirmModal: false + showConfirmModal: false, + isSendingEmails: false }; } @@ -89,10 +90,13 @@ export default class InviteMemberModal extends React.Component { var data = {}; data.invites = invites; + this.setState({isSendingEmails: true}); + Client.inviteMembers( data, () => { this.handleHide(false); + this.setState({isSendingEmails: false}); }, (err) => { if (err.message === 'This person is already on your team') { @@ -101,6 +105,8 @@ export default class InviteMemberModal extends React.Component { } else { this.setState({serverError: err.message}); } + + this.setState({isSendingEmails: false}); } ); } @@ -289,11 +295,6 @@ export default class InviteMemberModal extends React.Component { var content = null; var sendButton = null; - var sendButtonLabel = 'Send Invitation'; - if (this.state.inviteIds.length > 1) { - sendButtonLabel = 'Send Invitations'; - } - if (this.state.emailEnabled) { content = ( <div> @@ -309,14 +310,25 @@ export default class InviteMemberModal extends React.Component { </div> ); - sendButton = - ( - <button - onClick={this.handleSubmit} - type='button' - className='btn btn-primary' - >{sendButtonLabel}</button> + var sendButtonLabel = 'Send Invitation'; + if (this.state.isSendingEmails) { + sendButtonLabel = ( + <span><i className='fa fa-spinner fa-spin' />{' Sending'}</span> ); + } else if (this.state.inviteIds.length > 1) { + sendButtonLabel = 'Send Invitations'; + } + + sendButton = ( + <button + onClick={this.handleSubmit} + type='button' + className='btn btn-primary' + disabled={this.state.isSendingEmails} + > + {sendButtonLabel} + </button> + ); } else { var teamInviteLink = null; if (currentUser && TeamStore.getCurrent().type === 'O') { @@ -351,12 +363,13 @@ export default class InviteMemberModal extends React.Component { return ( <div> <Modal - className='modal-invite-member' + dialogClassName='modal-invite-member' show={this.state.show} onHide={this.handleHide.bind(this, true)} enforceFocus={!this.state.showConfirmModal} + backdrop={this.state.isSendingEmails ? 'static' : true} > - <Modal.Header closeButton={true}> + <Modal.Header closeButton={!this.state.isSendingEmails}> <Modal.Title>{'Invite New Member'}</Modal.Title> </Modal.Header> <Modal.Body ref='modalBody'> @@ -370,6 +383,7 @@ export default class InviteMemberModal extends React.Component { type='button' className='btn btn-default' onClick={this.handleHide.bind(this, true)} + disabled={this.state.isSendingEmails} > {'Cancel'} </button> diff --git a/web/react/components/popover_list_members.jsx b/web/react/components/popover_list_members.jsx index f3c0fa0b4..bd6b6d3bd 100644 --- a/web/react/components/popover_list_members.jsx +++ b/web/react/components/popover_list_members.jsx @@ -69,7 +69,6 @@ export default class PopoverListMembers extends React.Component { render() { let popoverHtml = []; - let count = 0; let countText = '-'; const members = this.props.members; const teamMembers = UserStore.getProfilesUsernameMap(); @@ -147,10 +146,10 @@ export default class PopoverListMembers extends React.Component { </div> </div> ); - count++; } }); + const count = this.props.memberCount; if (count > 20) { countText = '20+'; } else if (count > 0) { @@ -195,5 +194,6 @@ export default class PopoverListMembers extends React.Component { PopoverListMembers.propTypes = { members: React.PropTypes.array.isRequired, + memberCount: React.PropTypes.number.isRequired, channelId: React.PropTypes.string.isRequired }; diff --git a/web/react/components/posts_view.jsx b/web/react/components/posts_view.jsx index e62a04d24..087ca1df2 100644 --- a/web/react/components/posts_view.jsx +++ b/web/react/components/posts_view.jsx @@ -104,11 +104,13 @@ export default class PostsView extends React.Component { // check if it's the last comment in a consecutive string of comments on the same post // it is the last comment if it is last post in the channel or the next post has a different root post - var isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id); + const isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id); - var postCtl = ( + const keyPrefix = post.id ? post.id : i; + + const postCtl = ( <Post - key={post.id + 'postKey'} + key={keyPrefix + 'postKey'} ref={post.id} sameUser={sameUser} sameRoot={sameRoot} diff --git a/web/react/components/sidebar.jsx b/web/react/components/sidebar.jsx index c51eea9f6..b02ec0692 100644 --- a/web/react/components/sidebar.jsx +++ b/web/react/components/sidebar.jsx @@ -106,6 +106,8 @@ export default class Sidebar extends React.Component { const currentChannelId = ChannelStore.getCurrentId(); const channels = Object.assign([], ChannelStore.getAll()); + channels.sort((a, b) => a.display_name.localeCompare(b.display_name)); + const publicChannels = channels.filter((channel) => channel.type === Constants.OPEN_CHANNEL); const privateChannels = channels.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL); const directChannels = channels.filter((channel) => channel.type === Constants.DM_CHANNEL); diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx index 82f830038..e6530b941 100644 --- a/web/react/components/textbox.jsx +++ b/web/react/components/textbox.jsx @@ -243,7 +243,6 @@ export default class Textbox extends React.Component { const lht = parseInt($(e).css('lineHeight'), 10); const lines = e.scrollHeight / lht; - const previewLinkHeightMod = 20; let mod = 15; if (lines < 2.5 || this.props.messageText === '') { @@ -252,17 +251,17 @@ export default class Textbox extends React.Component { if (e.scrollHeight - mod < 167) { $(e).css({height: 'auto', 'overflow-y': 'hidden'}).height(e.scrollHeight - mod); - $(w).css({height: 'auto'}).height(e.scrollHeight + 2 + previewLinkHeightMod); + $(w).css({height: 'auto'}).height(e.scrollHeight + 2); $(w).closest('.post-body__cell').removeClass('scroll'); if (this.state.preview) { $(ReactDOM.findDOMNode(this.refs.preview)).css({height: 'auto', 'overflow-y': 'auto'}).height(e.scrollHeight - mod); } } else { $(e).css({height: 'auto', 'overflow-y': 'scroll'}).height(167 - mod); - $(w).css({height: 'auto'}).height(167 + previewLinkHeightMod); + $(w).css({height: 'auto'}).height(163); $(w).closest('.post-body__cell').addClass('scroll'); if (this.state.preview) { - $(ReactDOM.findDOMNode(this.refs.preview)).css({height: 'auto', 'overflow-y': 'scroll'}).height(167 - mod); + $(ReactDOM.findDOMNode(this.refs.preview)).css({height: 'auto', 'overflow-y': 'scroll'}).height(163); } } diff --git a/web/sass-files/sass/partials/_files.scss b/web/sass-files/sass/partials/_files.scss index d3ab3b9f8..49fb8e847 100644 --- a/web/sass-files/sass/partials/_files.scss +++ b/web/sass-files/sass/partials/_files.scss @@ -1,5 +1,6 @@ .preview-container { position: relative; + margin-top: 10px; width: 100%; max-height: 110px; height: 110px; diff --git a/web/sass-files/sass/partials/_navbar.scss b/web/sass-files/sass/partials/_navbar.scss index 2e78a8728..c06feffcf 100644 --- a/web/sass-files/sass/partials/_navbar.scss +++ b/web/sass-files/sass/partials/_navbar.scss @@ -96,9 +96,9 @@ } .badge-notify { - background:red; + background: red; position: absolute; - right: -5px; - top: -5px; + left: 4px; + top: 3px; z-index: 100; } diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index 3e2d6f045..79a97fbf9 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -46,21 +46,22 @@ body.ios { .textarea-wrapper { position:relative; - min-height:57px; - .textbox-preview-area { - position: absolute; - z-index: 2; - top: 0; - left: 0; - box-shadow: none; - } - .textbox-preview-link { - position: absolute; - z-index: 3; - bottom: 0; - right: 10px; - cursor: pointer; - } + min-height: 36px; + .textbox-preview-area { + position: absolute; + z-index: 2; + top: 0; + left: 0; + box-shadow: none; + } + .textbox-preview-link { + position: absolute; + z-index: 3; + bottom: -23px; + right: 0; + font-size: 13px; + cursor: pointer; + } } .date-separator, .new-separator { @@ -338,9 +339,9 @@ body.ios { } } .msg-typing { - min-height: 20px; - line-height: 18px; - display: inline-block; + min-height: 25px; + line-height: 25px; + display: block; font-size: 13px; @include opacity(0.7); } diff --git a/web/sass-files/sass/partials/_post_right.scss b/web/sass-files/sass/partials/_post_right.scss index c1d291073..ba41d3b95 100644 --- a/web/sass-files/sass/partials/_post_right.scss +++ b/web/sass-files/sass/partials/_post_right.scss @@ -13,6 +13,7 @@ &.post--root { padding: 1em 1em 0; margin: 0 0 1em; + width: 100%; hr { border-color: #DDD; margin: 1em 0 0 0; @@ -21,9 +22,10 @@ } .post-create__container { + width: 100%; margin-top: 10px; .textarea-wrapper { - min-height: 120px; + min-height: 100px; } .custom-textarea { min-height: 100px; @@ -31,10 +33,18 @@ .msg-typing { @include opacity(0.7); float: left; - padding-top: 17px; + margin-top: 3px; + font-size: 13px; + line-height: 20px; + min-width: 1px; + display: block; + height: 20px; + max-width: 200px; + @include clearfix; } .post-create-footer { - padding-top: 10px; + width: 100%; + padding-top: 5px; } .post-right-comments-upload-in-progress { padding: 6px 0; diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index 339412b45..cb140dce6 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -507,8 +507,16 @@ form { padding: 0; } + .post-create-footer { + .msg-typing { + margin-left: 45px; + width: 55%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } .post-create-body { - padding-bottom: 10px; display: table; width: 100%; .post-body__cell { @@ -532,11 +540,10 @@ display: table-cell; } } - .post-create-footer .msg-typing { - display: none; - } } .preview-container { + padding: 0px 10px; + margin-top: 20px; .preview-div { margin-top: 0; } diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss index b304450bc..0d75a42df 100644 --- a/web/sass-files/sass/partials/_settings.scss +++ b/web/sass-files/sass/partials/_settings.scss @@ -64,6 +64,10 @@ } } } + .profile-img { + width: 128px; + height: 128px; + } .settings-table { display: table; table-layout: fixed; |