summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/cli_test.go4
-rw-r--r--api/user.go46
-rw-r--r--api/user_test.go16
-rw-r--r--config/config.json5
-rw-r--r--i18n/en.json4
-rw-r--r--model/client.go8
-rw-r--r--model/config.go13
-rw-r--r--store/sql_user_store.go70
-rw-r--r--store/sql_user_store_test.go25
-rw-r--r--store/store.go2
-rw-r--r--utils/config.go1
-rw-r--r--webapp/action_creators/global_actions.jsx9
-rw-r--r--webapp/client/client.jsx11
-rw-r--r--webapp/components/admin_console/team_settings.jsx40
-rw-r--r--webapp/components/create_team/components/team_url.jsx2
-rw-r--r--webapp/components/filtered_user_list.jsx90
-rw-r--r--webapp/components/more_direct_channels.jsx79
-rw-r--r--webapp/components/sidebar.jsx35
-rw-r--r--webapp/i18n/en.json9
-rw-r--r--webapp/i18n/es.json2
-rw-r--r--webapp/i18n/fr.json2
-rw-r--r--webapp/i18n/ja.json2
-rw-r--r--webapp/i18n/pt.json2
-rw-r--r--webapp/sass/components/_modal.scss15
-rw-r--r--webapp/stores/user_store.jsx41
-rw-r--r--webapp/tests/client_user.test.jsx14
-rw-r--r--webapp/utils/async_client.jsx22
-rw-r--r--webapp/utils/constants.jsx1
-rw-r--r--webapp/utils/utils.jsx7
29 files changed, 505 insertions, 72 deletions
diff --git a/api/cli_test.go b/api/cli_test.go
index 6597819b3..12f38b54a 100644
--- a/api/cli_test.go
+++ b/api/cli_test.go
@@ -68,7 +68,7 @@ func TestCliCreateUserWithTeam(t *testing.T) {
t.Fatal(err)
}
- profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetProfilesForTeam(th.SystemAdminTeam.Id, "")).Data.(map[string]*model.User)
+ profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetProfiles(th.SystemAdminTeam.Id, "")).Data.(map[string]*model.User)
found := false
@@ -149,7 +149,7 @@ func TestCliJoinTeam(t *testing.T) {
t.Fatal(err)
}
- profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetProfilesForTeam(th.SystemAdminTeam.Id, "")).Data.(map[string]*model.User)
+ profiles := th.SystemAdminClient.Must(th.SystemAdminClient.GetProfiles(th.SystemAdminTeam.Id, "")).Data.(map[string]*model.User)
found := false
diff --git a/api/user.go b/api/user.go
index aee4dab61..f7d3eb1d3 100644
--- a/api/user.go
+++ b/api/user.go
@@ -55,6 +55,7 @@ func InitUser() {
BaseRoutes.Users.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("POST")
BaseRoutes.Users.Handle("/direct_profiles", ApiUserRequired(getDirectProfiles)).Methods("GET")
BaseRoutes.Users.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET")
+ BaseRoutes.Users.Handle("/profiles_for_dm_list/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfilesForDirectMessageList)).Methods("GET")
BaseRoutes.Users.Handle("/mfa", ApiAppHandler(checkMfa)).Methods("POST")
BaseRoutes.Users.Handle("/generate_mfa_qr", ApiUserRequiredTrustRequester(generateMfaQrCode)).Methods("GET")
@@ -902,6 +903,49 @@ func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
+func getProfilesForDirectMessageList(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ id := params["id"]
+
+ var pchan store.StoreChannel
+
+ if *utils.Cfg.TeamSettings.RestrictDirectMessage == model.DIRECT_MESSAGE_TEAM {
+ if c.Session.GetTeamByTeamId(id) == nil {
+ if !c.HasSystemAdminPermissions("getProfiles") {
+ return
+ }
+ }
+
+ pchan = Srv.Store.User().GetProfiles(id)
+ } else {
+ pchan = Srv.Store.User().GetAllProfiles()
+ }
+
+ if result := <-pchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ profiles := result.Data.(map[string]*model.User)
+
+ for k, p := range profiles {
+ options := utils.Cfg.GetSanitizeOptions()
+ options["passwordupdate"] = false
+
+ if c.IsSystemAdmin() {
+ options["fullname"] = true
+ options["email"] = true
+ } else {
+ p.ClearNonProfileFields()
+ }
+
+ p.Sanitize(options)
+ profiles[k] = p
+ }
+
+ w.Write([]byte(model.UserMapToJson(profiles)))
+ }
+}
+
func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
@@ -940,7 +984,6 @@ func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) {
w.Header().Set(model.HEADER_ETAG_SERVER, etag)
w.Write([]byte(model.UserMapToJson(profiles)))
- return
}
}
@@ -973,7 +1016,6 @@ func getDirectProfiles(c *Context, w http.ResponseWriter, r *http.Request) {
w.Header().Set(model.HEADER_ETAG_SERVER, etag)
w.Write([]byte(model.UserMapToJson(profiles)))
- return
}
}
diff --git a/api/user_test.go b/api/user_test.go
index ee13f43f2..629f7d257 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -395,6 +395,22 @@ func TestGetDirectProfiles(t *testing.T) {
}
}
+func TestGetProfilesForDirectMessageList(t *testing.T) {
+ th := Setup().InitBasic()
+
+ th.BasicClient.Must(th.BasicClient.CreateDirectChannel(th.BasicUser2.Id))
+
+ if result, err := th.BasicClient.GetProfilesForDirectMessageList(th.BasicTeam.Id); err != nil {
+ t.Fatal(err)
+ } else {
+ users := result.Data.(map[string]*model.User)
+
+ if len(users) < 1 {
+ t.Fatal("map was wrong length")
+ }
+ }
+}
+
func TestGetAudits(t *testing.T) {
th := Setup()
Client := th.CreateClient()
diff --git a/config/config.json b/config/config.json
index 3fe938fe2..b4fc84e0c 100644
--- a/config/config.json
+++ b/config/config.json
@@ -34,7 +34,8 @@
"RestrictCreationToDomains": "",
"RestrictTeamNames": true,
"EnableCustomBrand": false,
- "CustomBrandText": ""
+ "CustomBrandText": "",
+ "RestrictDirectMessage": "any"
},
"SqlSettings": {
"DriverName": "mysql",
@@ -155,4 +156,4 @@
"Directory": "./data/",
"EnableDaily": false
}
-} \ No newline at end of file
+}
diff --git a/i18n/en.json b/i18n/en.json
index 9a44ca31a..487f386a5 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -2304,6 +2304,10 @@
"translation": "Invalid connection security for LDAP settings. Must be '', 'TLS', or 'STARTTLS'"
},
{
+ "id": "model.config.is_valid.restrict_direct_message.app_error",
+ "translation": "Invalid direct message restriction. Must be 'any', or 'team'"
+ },
+ {
"id": "model.config.is_valid.listen_address.app_error",
"translation": "Invalid listen address for service settings Must be set."
},
diff --git a/model/client.go b/model/client.go
index 9285368c4..804b0d218 100644
--- a/model/client.go
+++ b/model/client.go
@@ -321,8 +321,8 @@ func (c *Client) GetMe(etag string) (*Result, *AppError) {
}
}
-func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) {
- if r, err := c.DoApiGet("/users/profiles/"+teamId, "", etag); err != nil {
+func (c *Client) GetProfilesForDirectMessageList(teamId string) (*Result, *AppError) {
+ if r, err := c.DoApiGet("/users/profiles_for_dm_list/"+teamId, "", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -330,8 +330,8 @@ func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) {
}
}
-func (c *Client) GetProfilesForTeam(teamId string, etag string) (*Result, *AppError) {
- if r, err := c.DoApiGet("/users/profiles/"+teamId+"?skip_direct=true", "", etag); err != nil {
+func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) {
+ if r, err := c.DoApiGet("/users/profiles/"+teamId, "", etag); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
diff --git a/model/config.go b/model/config.go
index b7c939202..872a5a68f 100644
--- a/model/config.go
+++ b/model/config.go
@@ -29,6 +29,9 @@ const (
GENERIC_NOTIFICATION = "generic"
FULL_NOTIFICATION = "full"
+ DIRECT_MESSAGE_ANY = "any"
+ DIRECT_MESSAGE_TEAM = "team"
+
FAKE_SETTING = "********************************"
)
@@ -162,6 +165,7 @@ type TeamSettings struct {
RestrictTeamNames *bool
EnableCustomBrand *bool
CustomBrandText *string
+ RestrictDirectMessage *string
}
type LdapSettings struct {
@@ -315,6 +319,11 @@ func (o *Config) SetDefaults() {
*o.TeamSettings.EnableOpenServer = false
}
+ if o.TeamSettings.RestrictDirectMessage == nil {
+ o.TeamSettings.RestrictDirectMessage = new(string)
+ *o.TeamSettings.RestrictDirectMessage = DIRECT_MESSAGE_ANY
+ }
+
if o.EmailSettings.EnableSignInWithEmail == nil {
o.EmailSettings.EnableSignInWithEmail = new(bool)
@@ -520,6 +529,10 @@ func (o *Config) IsValid() *AppError {
return NewLocAppError("Config.IsValid", "model.config.is_valid.max_users.app_error", nil, "")
}
+ if !(*o.TeamSettings.RestrictDirectMessage == DIRECT_MESSAGE_ANY || *o.TeamSettings.RestrictDirectMessage == DIRECT_MESSAGE_TEAM) {
+ return NewLocAppError("Config.IsValid", "model.config.is_valid.restrict_direct_message.app_error", nil, "")
+ }
+
if len(o.SqlSettings.AtRestEncryptKey) < 32 {
return NewLocAppError("Config.IsValid", "model.config.is_valid.encrypt_sql.app_error", nil, "")
}
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index 07e974559..8d4f1a31b 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -454,7 +454,34 @@ func (s SqlUserStore) GetEtagForDirectProfiles(userId string) StoreChannel {
Channels.Type = 'D'
AND Channels.Id = ChannelMembers.ChannelId
AND ChannelMembers.UserId = :UserId))
- `, map[string]interface{}{"UserId": userId})
+ OR Id IN (SELECT
+ Name
+ FROM
+ Preferences
+ WHERE
+ UserId = :UserId
+ AND Category = 'direct_channel_show')
+ `, map[string]interface{}{"UserId": userId})
+ if err != nil {
+ result.Data = fmt.Sprintf("%v.%v", model.CurrentVersion, model.GetMillis())
+ } else {
+ result.Data = fmt.Sprintf("%v.%v", model.CurrentVersion, updateAt)
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
+func (s SqlUserStore) GetEtagForAllProfiles() StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ updateAt, err := s.GetReplica().SelectInt("SELECT UpdateAt FROM Users ORDER BY UpdateAt DESC LIMIT 1")
if err != nil {
result.Data = fmt.Sprintf("%v.%v", model.CurrentVersion, model.GetMillis())
} else {
@@ -468,6 +495,37 @@ func (s SqlUserStore) GetEtagForDirectProfiles(userId string) StoreChannel {
return storeChannel
}
+func (us SqlUserStore) GetAllProfiles() StoreChannel {
+
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ var users []*model.User
+
+ if _, err := us.GetReplica().Select(&users, "SELECT * FROM Users"); err != nil {
+ result.Err = model.NewLocAppError("SqlUserStore.GetProfiles", "store.sql_user.get_profiles.app_error", nil, err.Error())
+ } else {
+
+ userMap := make(map[string]*model.User)
+
+ for _, u := range users {
+ u.Password = ""
+ u.AuthData = ""
+ userMap[u.Id] = u
+ }
+
+ result.Data = userMap
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (s SqlUserStore) GetEtagForProfiles(teamId string) StoreChannel {
storeChannel := make(StoreChannel)
@@ -548,7 +606,15 @@ func (us SqlUserStore) GetDirectProfiles(userId string) StoreChannel {
WHERE
Channels.Type = 'D'
AND Channels.Id = ChannelMembers.ChannelId
- AND ChannelMembers.UserId = :UserId))`, map[string]interface{}{"UserId": userId}); err != nil {
+ AND ChannelMembers.UserId = :UserId))
+ OR Id IN (SELECT
+ Name
+ FROM
+ Preferences
+ WHERE
+ UserId = :UserId
+ AND Category = 'direct_channel_show')
+ `, map[string]interface{}{"UserId": userId}); err != nil {
result.Err = model.NewLocAppError("SqlUserStore.GetDirectProfiles", "store.sql_user.get_profiles.app_error", nil, err.Error())
} else {
diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go
index fbe2285a9..b48da55f5 100644
--- a/store/sql_user_store_test.go
+++ b/store/sql_user_store_test.go
@@ -263,6 +263,31 @@ func TestActiveUserCount(t *testing.T) {
}
}
+func TestUserStoreGetAllProfiles(t *testing.T) {
+ Setup()
+
+ teamId := model.NewId()
+
+ u1 := &model.User{}
+ u1.Email = model.NewId()
+ Must(store.User().Save(u1))
+ Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u1.Id}))
+
+ u2 := &model.User{}
+ u2.Email = model.NewId()
+ Must(store.User().Save(u2))
+ Must(store.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: u2.Id}))
+
+ if r1 := <-store.User().GetAllProfiles(); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ users := r1.Data.(map[string]*model.User)
+ if len(users) < 2 {
+ t.Fatal("invalid returned users")
+ }
+ }
+}
+
func TestUserStoreGetProfiles(t *testing.T) {
Setup()
diff --git a/store/store.go b/store/store.go
index 8097fd8e8..7801f78f9 100644
--- a/store/store.go
+++ b/store/store.go
@@ -131,6 +131,7 @@ type UserStore interface {
UpdateMfaActive(userId string, active bool) StoreChannel
Get(id string) StoreChannel
GetAll() StoreChannel
+ GetAllProfiles() StoreChannel
GetProfiles(teamId string) StoreChannel
GetDirectProfiles(userId string) StoreChannel
GetProfileByIds(userId []string) StoreChannel
@@ -139,6 +140,7 @@ type UserStore interface {
GetByUsername(username string) StoreChannel
GetForLogin(loginId string, allowSignInWithUsername, allowSignInWithEmail, ldapEnabled bool) StoreChannel
VerifyEmail(userId string) StoreChannel
+ GetEtagForAllProfiles() StoreChannel
GetEtagForProfiles(teamId string) StoreChannel
GetEtagForDirectProfiles(userId string) StoreChannel
UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel
diff --git a/utils/config.go b/utils/config.go
index 14d6589a2..b0983892a 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -209,6 +209,7 @@ func getClientConfig(c *model.Config) map[string]string {
props["EnableUserCreation"] = strconv.FormatBool(c.TeamSettings.EnableUserCreation)
props["EnableOpenServer"] = strconv.FormatBool(*c.TeamSettings.EnableOpenServer)
props["RestrictTeamNames"] = strconv.FormatBool(*c.TeamSettings.RestrictTeamNames)
+ props["RestrictDirectMessage"] = *c.TeamSettings.RestrictDirectMessage
props["EnableOAuthServiceProvider"] = strconv.FormatBool(c.ServiceSettings.EnableOAuthServiceProvider)
props["SegmentDeveloperKey"] = c.ServiceSettings.SegmentDeveloperKey
diff --git a/webapp/action_creators/global_actions.jsx b/webapp/action_creators/global_actions.jsx
index 335c20219..ae7352e5d 100644
--- a/webapp/action_creators/global_actions.jsx
+++ b/webapp/action_creators/global_actions.jsx
@@ -157,6 +157,11 @@ export function emitPostFocusEvent(postId) {
);
}
+export function emitProfilesForDmList() {
+ AsyncClient.getProfilesForDirectMessageList();
+ AsyncClient.getTeamMembers(TeamStore.getCurrentId());
+}
+
export function emitCloseRightHandSide() {
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_SEARCH,
@@ -335,6 +340,10 @@ export function emitClearSuggestions(suggestionId) {
}
export function emitPreferenceChangedEvent(preference) {
+ if (preference.category === Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW) {
+ AsyncClient.getDirectProfiles();
+ }
+
AppDispatcher.handleServerAction({
type: Constants.ActionTypes.RECEIVED_PREFERENCE,
preference
diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx
index 69f573eff..73cc6120f 100644
--- a/webapp/client/client.jsx
+++ b/webapp/client/client.jsx
@@ -862,13 +862,22 @@ export default class Client {
getProfilesForTeam = (teamId, success, error) => {
request.
- get(`${this.getUsersRoute()}/profiles/${teamId}?skip_direct=true`).
+ get(`${this.getUsersRoute()}/profiles/${teamId}`).
set(this.defaultHeaders).
type('application/json').
accept('application/json').
end(this.handleResponse.bind(this, 'getProfilesForTeam', success, error));
}
+ getProfilesForDirectMessageList = (success, error) => {
+ request.
+ get(`${this.getUsersRoute()}/profiles_for_dm_list/${this.getTeamId()}`).
+ set(this.defaultHeaders).
+ type('application/json').
+ accept('application/json').
+ end(this.handleResponse.bind(this, 'getProfilesForDirectMessageList', success, error));
+ }
+
getStatuses = (ids, success, error) => {
request.
post(`${this.getUsersRoute()}/status`).
diff --git a/webapp/components/admin_console/team_settings.jsx b/webapp/components/admin_console/team_settings.jsx
index bbb7ec3c4..e7bfcd74a 100644
--- a/webapp/components/admin_console/team_settings.jsx
+++ b/webapp/components/admin_console/team_settings.jsx
@@ -24,6 +24,14 @@ const holders = defineMessages({
saving: {
id: 'admin.team.saving',
defaultMessage: 'Saving Config...'
+ },
+ restrictDirectMessageAny: {
+ id: 'admin.team.restrict_direct_message_any',
+ defaultMessage: 'Any user on the Mattermost server'
+ },
+ restrictDirectMessageTeam: {
+ id: 'admin.team.restrict_direct_message_team',
+ defaultMessage: 'Any member of the team'
}
});
@@ -48,6 +56,7 @@ class TeamSettings extends React.Component {
saveNeeded: false,
brandImageExists: false,
enableCustomBrand: this.props.config.TeamSettings.EnableCustomBrand,
+ restrictDirectMessage: this.props.config.TeamSettings.RestrictDirectMessage,
serverError: null
};
}
@@ -104,6 +113,7 @@ class TeamSettings extends React.Component {
config.TeamSettings.EnableUserCreation = this.refs.EnableUserCreation.checked;
config.TeamSettings.EnableOpenServer = this.refs.EnableOpenServer.checked;
config.TeamSettings.RestrictTeamNames = this.refs.RestrictTeamNames.checked;
+ config.TeamSettings.RestrictDirectMessage = this.refs.RestrictDirectMessage.value.trim();
if (this.refs.EnableCustomBrand) {
config.TeamSettings.EnableCustomBrand = this.refs.EnableCustomBrand.checked;
@@ -660,6 +670,36 @@ class TeamSettings extends React.Component {
</div>
</div>
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='restrictDirectMessage'
+ >
+ <FormattedMessage
+ id='admin.team.restrictDirectMessage'
+ defaultMessage='Enable users to open Direct Message channels with:'
+ />
+ </label>
+ <div className='col-sm-8'>
+ <select
+ className='form-control'
+ id='restrictDirectMessage'
+ ref='RestrictDirectMessage'
+ defaultValue={this.props.config.TeamSettings.RestrictDirectMessage}
+ onChange={this.handleChange.bind(this, 'restrictDirectMessage')}
+ >
+ <option value='any'>{formatMessage(holders.restrictDirectMessageAny)}</option>
+ <option value='team'>{formatMessage(holders.restrictDirectMessageTeam)}</option>
+ </select>
+ <p className='help-text'>
+ <FormattedHTMLMessage
+ id='admin.team.restrictDirectMessageDesc'
+ defaultMessage='"Any user on the Mattermost server" enables users to open a Direct Message channel with any user on the server, even if they are not on any teams together. "Any member of the team" limits the ability to open Direct Message channels to only users who are in the same team.'
+ />
+ </p>
+ </div>
+ </div>
+
{brand}
<div className='form-group'>
diff --git a/webapp/components/create_team/components/team_url.jsx b/webapp/components/create_team/components/team_url.jsx
index 1a20066b5..34e696938 100644
--- a/webapp/components/create_team/components/team_url.jsx
+++ b/webapp/components/create_team/components/team_url.jsx
@@ -5,6 +5,7 @@ import $ from 'jquery';
import ReactDOM from 'react-dom';
import * as Utils from 'utils/utils.jsx';
import Client from 'utils/web_client.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import Constants from 'utils/constants.jsx';
@@ -99,6 +100,7 @@ class TeamUrl extends React.Component {
(team) => {
Client.track('signup', 'signup_team_08_complete');
$('#sign-up-button').button('reset');
+ AsyncClient.getDirectProfiles();
TeamStore.saveTeam(team);
TeamStore.appendTeamMember({team_id: team.id, user_id: UserStore.getCurrentId(), roles: 'admin'});
TeamStore.emitChange();
diff --git a/webapp/components/filtered_user_list.jsx b/webapp/components/filtered_user_list.jsx
index e12faa298..83747c03d 100644
--- a/webapp/components/filtered_user_list.jsx
+++ b/webapp/components/filtered_user_list.jsx
@@ -15,6 +15,14 @@ const holders = defineMessages({
search: {
id: 'filtered_user_list.search',
defaultMessage: 'Search members'
+ },
+ anyTeam: {
+ id: 'filtered_user_list.any_team',
+ defaultMessage: 'All Users'
+ },
+ teamOnly: {
+ id: 'filtered_user_list.team_only',
+ defaultMessage: 'Members of this Team'
}
});
@@ -25,9 +33,13 @@ class FilteredUserList extends React.Component {
super(props);
this.handleFilterChange = this.handleFilterChange.bind(this);
+ this.handleListChange = this.handleListChange.bind(this);
+ this.filterUsers = this.filterUsers.bind(this);
this.state = {
- filter: ''
+ filter: '',
+ users: this.filterUsers(props.teamMembers, props.users),
+ selected: 'team'
};
}
@@ -37,18 +49,49 @@ class FilteredUserList extends React.Component {
}
}
+ filterUsers(teamMembers, users) {
+ if (!teamMembers || teamMembers.length === 0) {
+ return users;
+ }
+
+ var filteredUsers = users.filter((user) => {
+ for (const index in teamMembers) {
+ if (teamMembers.hasOwnProperty(index) && teamMembers[index].user_id === user.id) {
+ return true;
+ }
+ }
+
+ return false;
+ });
+
+ return filteredUsers;
+ }
+
handleFilterChange(e) {
this.setState({
filter: e.target.value
});
}
+ handleListChange(e) {
+ var users = this.props.users;
+
+ if (e.target.value === 'team') {
+ users = this.filterUsers(this.props.teamMembers, this.props.users);
+ }
+
+ this.setState({
+ selected: e.target.value,
+ users
+ });
+ }
+
render() {
const {formatMessage} = this.props.intl;
- let users = this.props.users;
+ let users = this.state.users;
- if (this.state.filter) {
+ if (this.state.filter && this.state.filter.length > 0) {
const filter = this.state.filter.toLowerCase();
users = users.filter((user) => {
@@ -60,7 +103,7 @@ class FilteredUserList extends React.Component {
}
let count;
- if (users.length === this.props.users.length) {
+ if (users.length === this.state.users.length) {
count = (
<FormattedMessage
id='filtered_user_list.count'
@@ -77,12 +120,42 @@ class FilteredUserList extends React.Component {
defaultMessage='{count} {count, plural, =0 {0 members} one {member} other {members}} of {total} Total'
values={{
count: users.length,
- total: this.props.users.length
+ total: this.state.users.length
}}
/>
);
}
+ let teamToggle;
+
+ let teamMembers = this.props.teamMembers;
+ if (this.props.showTeamToggle) {
+ teamMembers = [];
+
+ teamToggle = (
+ <div className='col-sm-6'>
+ <select
+ className='form-control member-select'
+ id='restrictList'
+ ref='restrictList'
+ defaultValue='team'
+ onChange={this.handleListChange}
+ >
+ <option value='any'>{formatMessage(holders.anyTeam)}</option>
+ <option value='team'>{formatMessage(holders.teamOnly)}</option>
+ </select>
+ <span
+ className='member-show'
+ >
+ <FormattedMessage
+ id='filtered_user_list.show'
+ defaultMessage='Show'
+ />
+ </span>
+ </div>
+ );
+ }
+
return (
<div
className='filtered-user-list'
@@ -100,6 +173,7 @@ class FilteredUserList extends React.Component {
<div className='col-sm-6'>
<span className='member-count'>{count}</span>
</div>
+ {teamToggle}
</div>
<div
ref='userList'
@@ -107,7 +181,7 @@ class FilteredUserList extends React.Component {
>
<UserList
users={users}
- teamMembers={this.props.teamMembers}
+ teamMembers={teamMembers}
actions={this.props.actions}
actionProps={this.props.actionProps}
/>
@@ -121,7 +195,8 @@ FilteredUserList.defaultProps = {
users: [],
teamMembers: [],
actions: [],
- actionProps: {}
+ actionProps: {},
+ showTeamToggle: false
};
FilteredUserList.propTypes = {
@@ -130,6 +205,7 @@ FilteredUserList.propTypes = {
teamMembers: React.PropTypes.arrayOf(React.PropTypes.object),
actions: React.PropTypes.arrayOf(React.PropTypes.func),
actionProps: React.PropTypes.object,
+ showTeamToggle: React.PropTypes.bool,
style: React.PropTypes.object
};
diff --git a/webapp/components/more_direct_channels.jsx b/webapp/components/more_direct_channels.jsx
index a7fb2b6cd..761ce0c37 100644
--- a/webapp/components/more_direct_channels.jsx
+++ b/webapp/components/more_direct_channels.jsx
@@ -4,11 +4,14 @@
import {Modal} from 'react-bootstrap';
import FilteredUserList from './filtered_user_list.jsx';
import UserStore from 'stores/user_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
import * as Utils from 'utils/utils.jsx';
+import * as GlobalActions from 'action_creators/global_actions.jsx';
import {FormattedMessage} from 'react-intl';
import {browserHistory} from 'react-router';
import SpinnerButton from 'components/spinner_button.jsx';
+import LoadingScreen from 'components/loading_screen.jsx';
import React from 'react';
@@ -17,38 +20,27 @@ export default class MoreDirectChannels extends React.Component {
super(props);
this.handleHide = this.handleHide.bind(this);
+ this.handleOnEnter = this.handleOnEnter.bind(this);
this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this);
this.handleUserChange = this.handleUserChange.bind(this);
+ this.onTeamChange = this.onTeamChange.bind(this);
this.createJoinDirectChannelButton = this.createJoinDirectChannelButton.bind(this);
this.state = {
- users: this.getUsersFromStore(),
+ users: null,
+ teamMembers: null,
loadingDMChannel: -1
};
}
- getUsersFromStore() {
- const currentId = UserStore.getCurrentId();
- const profiles = UserStore.getActiveOnlyProfiles();
- const users = [];
-
- for (const id in profiles) {
- if (id !== currentId) {
- users.push(profiles[id]);
- }
- }
-
- users.sort((a, b) => a.username.localeCompare(b.username));
-
- return users;
- }
-
componentDidMount() {
- UserStore.addChangeListener(this.handleUserChange);
+ UserStore.addDmListChangeListener(this.handleUserChange);
+ TeamStore.addChangeListener(this.onTeamChange);
}
componentWillUnmount() {
- UserStore.removeChangeListener(this.handleUserChange);
+ UserStore.removeDmListChangeListener(this.handleUserChange);
+ TeamStore.removeChangeListener(this.onTeamChange);
}
shouldComponentUpdate(nextProps, nextState) {
@@ -77,6 +69,17 @@ export default class MoreDirectChannels extends React.Component {
}
}
+ handleOnEnter() {
+ this.setState({
+ users: null,
+ teamMembers: null
+ });
+ }
+
+ handleOnEntered() {
+ GlobalActions.emitProfilesForDmList();
+ }
+
handleShowDirectChannel(teammate, e) {
e.preventDefault();
@@ -99,7 +102,15 @@ export default class MoreDirectChannels extends React.Component {
}
handleUserChange() {
- this.setState({users: this.getUsersFromStore()});
+ this.setState({
+ users: UserStore.getProfilesForDmList()
+ });
+ }
+
+ onTeamChange() {
+ this.setState({
+ teamMembers: TeamStore.getMembersForTeam()
+ });
}
createJoinDirectChannelButton({user}) {
@@ -123,11 +134,33 @@ export default class MoreDirectChannels extends React.Component {
maxHeight = Utils.windowHeight() - 300;
}
+ var body = null;
+ if (this.state.users == null || this.state.teamMembers == null) {
+ body = (<LoadingScreen/>);
+ } else {
+ var showTeamToggle = false;
+ if (global.window.mm_config.RestrictDirectMessage === 'any') {
+ showTeamToggle = true;
+ }
+
+ body = (
+ <FilteredUserList
+ style={{maxHeight}}
+ users={this.state.users}
+ teamMembers={this.state.teamMembers}
+ actions={[this.createJoinDirectChannelButton]}
+ showTeamToggle={showTeamToggle}
+ />
+ );
+ }
+
return (
<Modal
dialogClassName='more-modal more-direct-channels'
show={this.props.show}
onHide={this.handleHide}
+ onEnter={this.handleOnEnter}
+ onEntered={this.handleOnEntered}
>
<Modal.Header closeButton={true}>
<Modal.Title>
@@ -138,11 +171,7 @@ export default class MoreDirectChannels extends React.Component {
</Modal.Title>
</Modal.Header>
<Modal.Body>
- <FilteredUserList
- style={{maxHeight}}
- users={this.state.users}
- actions={[this.createJoinDirectChannelButton]}
- />
+ {body}
</Modal.Body>
<Modal.Footer>
<button
diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx
index 4ab8b6b40..4ce89d37c 100644
--- a/webapp/components/sidebar.jsx
+++ b/webapp/components/sidebar.jsx
@@ -124,8 +124,6 @@ export default class Sidebar extends React.Component {
directChannels.sort(this.sortChannelsByDisplayName);
- const hiddenDirectChannelCount = UserStore.getActiveOnlyProfileList(true).length - directChannels.length;
-
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
return {
@@ -134,7 +132,6 @@ export default class Sidebar extends React.Component {
publicChannels,
privateChannels,
directChannels,
- hiddenDirectChannelCount,
unreadCounts: JSON.parse(JSON.stringify(ChannelStore.getUnreadCounts())),
showTutorialTip: tutorialStep === TutorialSteps.CHANNEL_POPOVER,
currentTeam: TeamStore.getCurrent(),
@@ -527,25 +524,19 @@ export default class Sidebar extends React.Component {
}
head.appendChild(link);
- var directMessageMore = null;
- if (this.state.hiddenDirectChannelCount > 0) {
- directMessageMore = (
- <li key='more'>
- <a
- href='#'
- onClick={this.showMoreDirectChannelsModal}
- >
- <FormattedMessage
- id='sidebar.more'
- defaultMessage='More ({count})'
- values={{
- count: this.state.hiddenDirectChannelCount
- }}
- />
- </a>
- </li>
- );
- }
+ var directMessageMore = (
+ <li key='more'>
+ <a
+ href='#'
+ onClick={this.showMoreDirectChannelsModal}
+ >
+ <FormattedMessage
+ id='sidebar.more'
+ defaultMessage='More'
+ />
+ </a>
+ </li>
+ );
let showChannelModal = false;
if (this.state.newChannelModalType !== '') {
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index fbbf66fbd..da2acba3d 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -542,6 +542,10 @@
"admin.team.userCreationTitle": "Enable User Creation: ",
"admin.team_analytics.activeUsers": "Active Users With Posts",
"admin.team_analytics.totalPosts": "Total Posts",
+ "admin.team.restrictDirectMessage": "Enable users to open Direct Message channels with:",
+ "admin.team.restrictDirectMessageDesc": "'Any user on the Mattermost server' enables users to open a Direct Message channel with any user on the server, even if they are not on any teams together. 'Any member of the team' limits the ability to open Direct Message channels to only users who are in the same team.",
+ "admin.team.restrict_direct_message_any": "Any user on the Mattermost server",
+ "admin.team.restrict_direct_message_team": "Any member of the team",
"admin.userList.title": "Users for {team}",
"admin.userList.title2": "Users for {team} ({count})",
"admin.user_item.authServiceEmail": ", <strong>Sign-in Method:</strong> Email",
@@ -859,6 +863,9 @@
"file_upload.pasted": "Image Pasted at ",
"filtered_user_list.count": "{count} {count, plural, =0 {0 members} one {member} other {members}}",
"filtered_user_list.countTotal": "{count} {count, plural, =0 {0 members} one {member} other {members}} of {total} Total",
+ "filtered_user_list.show": "Show",
+ "filtered_user_list.any_team": "All Users",
+ "filtered_user_list.team_only": "Members of this Team",
"filtered_user_list.member": "Member",
"filtered_user_list.search": "Search members",
"find_team.email": "Email",
@@ -1120,7 +1127,7 @@
"sidebar.createChannel": "Create new channel",
"sidebar.createGroup": "Create new group",
"sidebar.direct": "Direct Messages",
- "sidebar.more": "More ({count})",
+ "sidebar.more": "More",
"sidebar.moreElips": "More...",
"sidebar.pg": "Private Groups",
"sidebar.removeList": "Remove from list",
diff --git a/webapp/i18n/es.json b/webapp/i18n/es.json
index 34e4360b5..b6189916e 100644
--- a/webapp/i18n/es.json
+++ b/webapp/i18n/es.json
@@ -1118,7 +1118,7 @@
"sidebar.createChannel": "Crear un nuevo canal",
"sidebar.createGroup": "Crear un nuevo grupo",
"sidebar.direct": "Mensajes Directos",
- "sidebar.more": "Más ({count})",
+ "sidebar.more": "Más",
"sidebar.moreElips": "Más...",
"sidebar.pg": "Grupos Privados",
"sidebar.removeList": "Remover de la lista",
diff --git a/webapp/i18n/fr.json b/webapp/i18n/fr.json
index c92dff181..31f646d84 100644
--- a/webapp/i18n/fr.json
+++ b/webapp/i18n/fr.json
@@ -976,7 +976,7 @@
"sidebar.createChannel": "Créer un nouveau canal",
"sidebar.createGroup": "Créer un nouveau groupe",
"sidebar.direct": "Messages privés",
- "sidebar.more": "Plus ({count})",
+ "sidebar.more": "Plus",
"sidebar.moreElips": "Plus...",
"sidebar.pg": "Groupes privés",
"sidebar.removeList": "Retirer de la liste",
diff --git a/webapp/i18n/ja.json b/webapp/i18n/ja.json
index 88e10498e..f6bc8cc7d 100644
--- a/webapp/i18n/ja.json
+++ b/webapp/i18n/ja.json
@@ -1078,7 +1078,7 @@
"sidebar.createChannel": "新しいチャンネルを作成する",
"sidebar.createGroup": "新しいグループを作成する",
"sidebar.direct": "ダイレクトメッセージ",
- "sidebar.more": "もっと表示する({count})",
+ "sidebar.more": "もっと表示する",
"sidebar.moreElips": "もっと…",
"sidebar.pg": "非公開グループ",
"sidebar.removeList": "一覧から削除する",
diff --git a/webapp/i18n/pt.json b/webapp/i18n/pt.json
index d10487c79..501727f98 100644
--- a/webapp/i18n/pt.json
+++ b/webapp/i18n/pt.json
@@ -1120,7 +1120,7 @@
"sidebar.createChannel": "Criar novo canal",
"sidebar.createGroup": "Criar um novo grupo",
"sidebar.direct": "Mensagens Diretas",
- "sidebar.more": "Mais ({count})",
+ "sidebar.more": "Mais",
"sidebar.moreElips": "Mais...",
"sidebar.pg": "Grupos Privados",
"sidebar.removeList": "Remover da lista",
diff --git a/webapp/sass/components/_modal.scss b/webapp/sass/components/_modal.scss
index f66a08804..90afe1b11 100644
--- a/webapp/sass/components/_modal.scss
+++ b/webapp/sass/components/_modal.scss
@@ -436,6 +436,21 @@
margin-top: 5px;
}
+ .member-show {
+ @include opacity(.8);
+ float: right;
+ margin-top: 12px;
+ margin-right: 3px;
+ }
+
+ .member-select {
+ @include opacity(.8);
+ float: right;
+ width: auto;
+ margin-top: 5px;
+ margin-right: 5px;
+ }
+
.more-purpose {
@include opacity(.7);
}
diff --git a/webapp/stores/user_store.jsx b/webapp/stores/user_store.jsx
index 8d685aea3..5a27d15ea 100644
--- a/webapp/stores/user_store.jsx
+++ b/webapp/stores/user_store.jsx
@@ -7,6 +7,7 @@ import EventEmitter from 'events';
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
+const CHANGE_EVENT_DM_LIST = 'change_dm_list';
const CHANGE_EVENT = 'change';
const CHANGE_EVENT_SESSIONS = 'change_sessions';
const CHANGE_EVENT_AUDITS = 'change_audits';
@@ -19,6 +20,7 @@ class UserStoreClass extends EventEmitter {
}
clear() {
+ this.profiles_for_dm_list = {};
this.profiles = {};
this.direct_profiles = {};
this.statuses = {};
@@ -40,6 +42,18 @@ class UserStoreClass extends EventEmitter {
this.removeListener(CHANGE_EVENT, callback);
}
+ emitDmListChange() {
+ this.emit(CHANGE_EVENT_DM_LIST);
+ }
+
+ addDmListChangeListener(callback) {
+ this.on(CHANGE_EVENT_DM_LIST, callback);
+ }
+
+ removeDmListChangeListener(callback) {
+ this.removeListener(CHANGE_EVENT_DM_LIST, callback);
+ }
+
emitSessionsChange() {
this.emit(CHANGE_EVENT_SESSIONS);
}
@@ -190,6 +204,29 @@ class UserStoreClass extends EventEmitter {
}
}
+ getProfilesForDmList() {
+ const currentId = this.getCurrentId();
+ const profiles = [];
+
+ for (const id in this.profiles_for_dm_list) {
+ if (this.profiles_for_dm_list.hasOwnProperty(id) && id !== currentId) {
+ var profile = this.profiles_for_dm_list[id];
+
+ if (profile.delete_at === 0) {
+ profiles.push(profile);
+ }
+ }
+ }
+
+ profiles.sort((a, b) => a.username.localeCompare(b.username));
+
+ return profiles;
+ }
+
+ saveProfilesForDmList(profiles) {
+ this.profiles_for_dm_list = profiles;
+ }
+
setSessions(sessions) {
this.sessions = sessions;
}
@@ -278,6 +315,10 @@ UserStore.dispatchToken = AppDispatcher.register((payload) => {
var action = payload.action;
switch (action.type) {
+ case ActionTypes.RECEIVED_PROFILES_FOR_DM_LIST:
+ UserStore.saveProfilesForDmList(action.profiles);
+ UserStore.emitDmListChange();
+ break;
case ActionTypes.RECEIVED_PROFILES:
UserStore.saveProfiles(action.profiles);
UserStore.emitChange();
diff --git a/webapp/tests/client_user.test.jsx b/webapp/tests/client_user.test.jsx
index 7835a38bf..610308fa3 100644
--- a/webapp/tests/client_user.test.jsx
+++ b/webapp/tests/client_user.test.jsx
@@ -480,6 +480,20 @@ describe('Client.User', function() {
});
});
+ it('getProfilesForDirectMessageList', function(done) {
+ TestHelper.initBasic(() => {
+ TestHelper.basicClient().getProfilesForDirectMessageList(
+ function(data) {
+ assert.equal(Object.keys(data).length > 0, true);
+ done();
+ },
+ function(err) {
+ done(new Error(err.message));
+ }
+ );
+ });
+ });
+
it('getStatuses', function(done) {
TestHelper.initBasic(() => {
var ids = [];
diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx
index 189b159e8..0dede3bc9 100644
--- a/webapp/utils/async_client.jsx
+++ b/webapp/utils/async_client.jsx
@@ -224,6 +224,28 @@ export function getTeamMembers(teamId) {
);
}
+export function getProfilesForDirectMessageList() {
+ if (isCallInProgress('getProfilesForDirectMessageList')) {
+ return;
+ }
+
+ callTracker.getProfilesForDirectMessageList = utils.getTimestamp();
+ Client.getProfilesForDirectMessageList(
+ (data) => {
+ callTracker.getProfilesForDirectMessageList = 0;
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_PROFILES_FOR_DM_LIST,
+ profiles: data
+ });
+ },
+ (err) => {
+ callTracker.getProfilesForDirectMessageList = 0;
+ dispatchError(err, 'getProfilesForDirectMessageList');
+ }
+ );
+}
+
export function getProfiles() {
if (isCallInProgress('getProfiles')) {
return;
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index 88bf56706..3ae99d7fa 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -60,6 +60,7 @@ export default {
RECEIVED_MENTION_DATA: null,
RECEIVED_ADD_MENTION: null,
+ RECEIVED_PROFILES_FOR_DM_LIST: null,
RECEIVED_PROFILES: null,
RECEIVED_DIRECT_PROFILES: null,
RECEIVED_ME: null,
diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx
index 8917c97e4..e34fa403f 100644
--- a/webapp/utils/utils.jsx
+++ b/webapp/utils/utils.jsx
@@ -1247,6 +1247,13 @@ export function openDirectChannelToUser(user, successCb, errorCb) {
'true'
);
+ // if the user in another team and isn't already in the direct message
+ // list then we should add him so his name shows up correctly.
+ var profileUser = UserStore.getProfile(user.id);
+ if (!profileUser) {
+ UserStore.getDirectProfiles()[user.id] = user;
+ }
+
const channelName = this.getDirectChannelName(UserStore.getCurrentId(), user.id);
let channel = ChannelStore.getByName(channelName);