summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/channel.go7
-rw-r--r--api/channel_test.go4
-rw-r--r--doc/developer/tests/test-links.md16
-rw-r--r--model/channel_extra.go5
-rw-r--r--store/sql_channel_store.go20
-rw-r--r--store/sql_channel_store_test.go8
-rw-r--r--store/store.go1
-rw-r--r--web/react/components/channel_header.jsx6
-rw-r--r--web/react/components/invite_member_modal.jsx44
-rw-r--r--web/react/components/popover_list_members.jsx4
-rw-r--r--web/react/components/posts_view.jsx8
-rw-r--r--web/react/components/sidebar.jsx2
-rw-r--r--web/react/components/textbox.jsx7
-rw-r--r--web/sass-files/sass/partials/_files.scss1
-rw-r--r--web/sass-files/sass/partials/_navbar.scss6
-rw-r--r--web/sass-files/sass/partials/_post.scss37
-rw-r--r--web/sass-files/sass/partials/_post_right.scss16
-rw-r--r--web/sass-files/sass/partials/_responsive.scss15
-rw-r--r--web/sass-files/sass/partials/_settings.scss4
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;