PLT-3077 Add group messaging (#5489)
* Implement server changes for group messaging * Majority of client-side implementation * Some server updates * Added new React multiselect component * Fix style issues * Add custom renderer for options * Fix model test * Update ENTER functionality for multiselect control * Remove buttons from multiselect UI control * Updating group messaging UI (#5524) * Move filter controls up a component level * Scroll with arrow keys * Updating mobile layout for multiselect (#5534) * Fix race condition when backspacing quickly * Hidden or new GMs show up for regular messages * Add overriding of number remaining text * Add UI filtering for team if config setting set * Add icon to channel switcher and class prop to status icon * Minor updates per feedback * Improving group messaging UI (#5563) * UX changes per feedback * Update email for group messages * UI fixes for group messaging (#5587) * Fix missing localization string * Add maximum users message when adding members to GM * Fix input clearing on Android * Updating group messaging UI (#5603) * Updating UI for group messaging (#5604)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
8c5cee9521
Коммит
3a91d4e5e4
@@ -25,6 +25,7 @@ func InitChannel() {
|
|||||||
BaseRoutes.Channels.Handle("/create", ApiUserRequired(createChannel)).Methods("POST")
|
BaseRoutes.Channels.Handle("/create", ApiUserRequired(createChannel)).Methods("POST")
|
||||||
BaseRoutes.Channels.Handle("/view", ApiUserRequired(viewChannel)).Methods("POST")
|
BaseRoutes.Channels.Handle("/view", ApiUserRequired(viewChannel)).Methods("POST")
|
||||||
BaseRoutes.Channels.Handle("/create_direct", ApiUserRequired(createDirectChannel)).Methods("POST")
|
BaseRoutes.Channels.Handle("/create_direct", ApiUserRequired(createDirectChannel)).Methods("POST")
|
||||||
|
BaseRoutes.Channels.Handle("/create_group", ApiUserRequired(createGroupChannel)).Methods("POST")
|
||||||
BaseRoutes.Channels.Handle("/update", ApiUserRequired(updateChannel)).Methods("POST")
|
BaseRoutes.Channels.Handle("/update", ApiUserRequired(updateChannel)).Methods("POST")
|
||||||
BaseRoutes.Channels.Handle("/update_header", ApiUserRequired(updateChannelHeader)).Methods("POST")
|
BaseRoutes.Channels.Handle("/update_header", ApiUserRequired(updateChannelHeader)).Methods("POST")
|
||||||
BaseRoutes.Channels.Handle("/update_purpose", ApiUserRequired(updateChannelPurpose)).Methods("POST")
|
BaseRoutes.Channels.Handle("/update_purpose", ApiUserRequired(updateChannelPurpose)).Methods("POST")
|
||||||
@@ -98,6 +99,38 @@ func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createGroupChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_CREATE_GROUP_CHANNEL) {
|
||||||
|
c.SetPermissionError(model.PERMISSION_CREATE_GROUP_CHANNEL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userIds := model.ArrayFromJson(r.Body)
|
||||||
|
if len(userIds) == 0 {
|
||||||
|
c.SetInvalidParam("createGroupChannel", "user_ids")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, id := range userIds {
|
||||||
|
if id == c.Session.UserId {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
userIds = append(userIds, c.Session.UserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sc, err := app.CreateGroupChannel(userIds); err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
w.Write([]byte(sc.ToJson()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func CanManageChannel(c *Context, channel *model.Channel) bool {
|
func CanManageChannel(c *Context, channel *model.Channel) bool {
|
||||||
if channel.Type == model.CHANNEL_OPEN && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES) {
|
if channel.Type == model.CHANNEL_OPEN && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES) {
|
||||||
c.SetPermissionError(model.PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES)
|
c.SetPermissionError(model.PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES)
|
||||||
@@ -457,7 +490,7 @@ func getChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if channel.TeamId != c.TeamId && channel.Type != model.CHANNEL_DIRECT {
|
if channel.TeamId != c.TeamId && !channel.IsGroupOrDirect() {
|
||||||
c.Err = model.NewLocAppError("getChannel", "api.channel.get_channel.wrong_team.app_error", map[string]interface{}{"ChannelId": id, "TeamId": c.TeamId}, "")
|
c.Err = model.NewLocAppError("getChannel", "api.channel.get_channel.wrong_team.app_error", map[string]interface{}{"ChannelId": id, "TeamId": c.TeamId}, "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -493,7 +526,7 @@ func getChannelByName(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if channel.TeamId != c.TeamId && channel.Type != model.CHANNEL_DIRECT {
|
if channel.TeamId != c.TeamId && !channel.IsGroupOrDirect() {
|
||||||
c.Err = model.NewLocAppError("getChannel", "api.channel.get_channel.wrong_team.app_error", map[string]interface{}{"ChannelName": channelName, "TeamId": c.TeamId}, "")
|
c.Err = model.NewLocAppError("getChannel", "api.channel.get_channel.wrong_team.app_error", map[string]interface{}{"ChannelName": channelName, "TeamId": c.TeamId}, "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,6 +219,46 @@ func TestCreateDirectChannel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateGroupChannel(t *testing.T) {
|
||||||
|
th := Setup().InitBasic()
|
||||||
|
Client := th.BasicClient
|
||||||
|
user := th.BasicUser
|
||||||
|
user2 := th.BasicUser2
|
||||||
|
user3 := th.CreateUser(Client)
|
||||||
|
|
||||||
|
userIds := []string{user.Id, user2.Id, user3.Id}
|
||||||
|
|
||||||
|
var channel *model.Channel
|
||||||
|
if result, err := Client.CreateGroupChannel(userIds); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
channel = result.Data.(*model.Channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel.Type != model.CHANNEL_GROUP {
|
||||||
|
t.Fatal("channel type was not group")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't fail on group channels already existing and return the original channel again
|
||||||
|
if result, err := Client.CreateGroupChannel(userIds); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if result.Data.(*model.Channel).Id != channel.Id {
|
||||||
|
t.Fatal("didn't return original group channel when saving a duplicate")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := Client.CreateGroupChannel([]string{user.Id}); err == nil {
|
||||||
|
t.Fatal("should have failed with not enough users")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := Client.CreateGroupChannel([]string{}); err == nil {
|
||||||
|
t.Fatal("should have failed with not enough users")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := Client.CreateGroupChannel([]string{user.Id, user2.Id, user3.Id, "junk"}); err == nil {
|
||||||
|
t.Fatal("should have failed with non-existent user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUpdateChannel(t *testing.T) {
|
func TestUpdateChannel(t *testing.T) {
|
||||||
th := Setup().InitBasic().InitSystemAdmin()
|
th := Setup().InitBasic().InitSystemAdmin()
|
||||||
Client := th.SystemAdminClient
|
Client := th.SystemAdminClient
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func JoinDefaultChannels(teamId string, user *model.User, channelRole string) *m
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CreateChannelWithUser(channel *model.Channel, userId string) (*model.Channel, *model.AppError) {
|
func CreateChannelWithUser(channel *model.Channel, userId string) (*model.Channel, *model.AppError) {
|
||||||
if channel.Type == model.CHANNEL_DIRECT {
|
if channel.IsGroupOrDirect() {
|
||||||
return nil, model.NewAppError("CreateChannelWithUser", "api.channel.create_channel.direct_channel.app_error", nil, "", http.StatusBadRequest)
|
return nil, model.NewAppError("CreateChannelWithUser", "api.channel.create_channel.direct_channel.app_error", nil, "", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +197,60 @@ func WaitForChannelMembership(channelId string, userId string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CreateGroupChannel(userIds []string) (*model.Channel, *model.AppError) {
|
||||||
|
if len(userIds) > model.CHANNEL_GROUP_MAX_USERS || len(userIds) < model.CHANNEL_GROUP_MIN_USERS {
|
||||||
|
return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_size.app_error", nil, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
var users []*model.User
|
||||||
|
if result := <-Srv.Store.User().GetProfileByIds(userIds, true); result.Err != nil {
|
||||||
|
return nil, result.Err
|
||||||
|
} else {
|
||||||
|
users = result.Data.([]*model.User)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(users) != len(userIds) {
|
||||||
|
return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_user.app_error", nil, "user_ids="+model.ArrayToJson(userIds), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
group := &model.Channel{
|
||||||
|
Name: model.GetGroupNameFromUserIds(userIds),
|
||||||
|
DisplayName: model.GetGroupDisplayNameFromUsers(users, true),
|
||||||
|
Type: model.CHANNEL_GROUP,
|
||||||
|
}
|
||||||
|
|
||||||
|
if result := <-Srv.Store.Channel().Save(group); result.Err != nil {
|
||||||
|
if result.Err.Id == store.CHANNEL_EXISTS_ERROR {
|
||||||
|
return result.Data.(*model.Channel), nil
|
||||||
|
} else {
|
||||||
|
return nil, result.Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel := result.Data.(*model.Channel)
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
cm := &model.ChannelMember{
|
||||||
|
UserId: user.Id,
|
||||||
|
ChannelId: group.Id,
|
||||||
|
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
||||||
|
Roles: model.ROLE_CHANNEL_USER.Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if result := <-Srv.Store.Channel().SaveMember(cm); result.Err != nil {
|
||||||
|
return nil, result.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
InvalidateCacheForUser(user.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_GROUP_ADDED, "", group.Id, "", nil)
|
||||||
|
message.Add("teammate_ids", model.ArrayToJson(userIds))
|
||||||
|
Publish(message)
|
||||||
|
|
||||||
|
return channel, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
|
func UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
|
||||||
if result := <-Srv.Store.Channel().Update(channel); result.Err != nil {
|
if result := <-Srv.Store.Channel().Update(channel); result.Err != nil {
|
||||||
return nil, result.Err
|
return nil, result.Err
|
||||||
@@ -702,7 +756,7 @@ func LeaveChannel(channelId string, userId string) *model.AppError {
|
|||||||
user := uresult.Data.(*model.User)
|
user := uresult.Data.(*model.User)
|
||||||
membersCount := ccmresult.Data.(int64)
|
membersCount := ccmresult.Data.(int64)
|
||||||
|
|
||||||
if channel.Type == model.CHANNEL_DIRECT {
|
if channel.IsGroupOrDirect() {
|
||||||
err := model.NewLocAppError("LeaveChannel", "api.channel.leave.direct.app_error", nil, "")
|
err := model.NewLocAppError("LeaveChannel", "api.channel.leave.direct.app_error", nil, "")
|
||||||
err.StatusCode = http.StatusBadRequest
|
err.StatusCode = http.StatusBadRequest
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -244,6 +244,8 @@ func renderBatchedPost(template *utils.HTMLTemplate, post *model.Post, teamName
|
|||||||
return ""
|
return ""
|
||||||
} else if channel := result.Data.(*model.Channel); channel.Type == model.CHANNEL_DIRECT {
|
} else if channel := result.Data.(*model.Channel); channel.Type == model.CHANNEL_DIRECT {
|
||||||
template.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.direct_message")
|
template.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.direct_message")
|
||||||
|
} else if channel.Type == model.CHANNEL_GROUP {
|
||||||
|
template.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.group_message")
|
||||||
} else {
|
} else {
|
||||||
template.Props["ChannelName"] = channel.DisplayName
|
template.Props["ChannelName"] = channel.DisplayName
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ func SendNotifications(post *model.Post, team *model.Team, channel *model.Channe
|
|||||||
}
|
}
|
||||||
|
|
||||||
senderName := make(map[string]string)
|
senderName := make(map[string]string)
|
||||||
|
channelName := make(map[string]string)
|
||||||
for _, id := range mentionedUsersList {
|
for _, id := range mentionedUsersList {
|
||||||
senderName[id] = ""
|
senderName[id] = ""
|
||||||
if post.IsSystemMessage() {
|
if post.IsSystemMessage() {
|
||||||
@@ -135,6 +136,19 @@ func SendNotifications(post *model.Post, team *model.Team, channel *model.Channe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if channel.Type == model.CHANNEL_GROUP {
|
||||||
|
userList := []*model.User{}
|
||||||
|
for _, u := range profileMap {
|
||||||
|
if u.Id != sender.Id && u.Id != id {
|
||||||
|
userList = append(userList, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userList = append(userList, sender)
|
||||||
|
channelName[id] = model.GetGroupDisplayNameFromUsers(userList, false)
|
||||||
|
} else {
|
||||||
|
channelName[id] = channel.DisplayName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var senderUsername string
|
var senderUsername string
|
||||||
@@ -259,7 +273,7 @@ func SendNotifications(post *model.Post, team *model.Team, channel *model.Channe
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], true, status, post) {
|
if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], true, status, post) {
|
||||||
sendPushNotification(post, profileMap[id], channel, senderName[id], true)
|
sendPushNotification(post, profileMap[id], channel, senderName[id], channelName[id], true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +286,7 @@ func SendNotifications(post *model.Post, team *model.Team, channel *model.Channe
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], false, status, post) {
|
if ShouldSendPushNotification(profileMap[id], channelMemberNotifyPropsMap[id], false, status, post) {
|
||||||
sendPushNotification(post, profileMap[id], channel, senderName[id], false)
|
sendPushNotification(post, profileMap[id], channel, senderName[id], channelName[id], false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,8 +327,8 @@ func SendNotifications(post *model.Post, team *model.Team, channel *model.Channe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sendNotificationEmail(post *model.Post, user *model.User, channel *model.Channel, team *model.Team, senderName string, sender *model.User) *model.AppError {
|
func sendNotificationEmail(post *model.Post, user *model.User, channel *model.Channel, team *model.Team, senderName string, sender *model.User) *model.AppError {
|
||||||
if channel.Type == model.CHANNEL_DIRECT && channel.TeamId != team.Id {
|
if channel.IsGroupOrDirect() && channel.TeamId != team.Id {
|
||||||
// this message is a cross-team DM so it we need to find a team that the recipient is on to use in the link
|
// this message is a cross-team DM/GM so we need to find a team that the recipient is on to use in the link
|
||||||
if result := <-Srv.Store.Team().GetTeamsByUserId(user.Id); result.Err != nil {
|
if result := <-Srv.Store.Team().GetTeamsByUserId(user.Id); result.Err != nil {
|
||||||
return result.Err
|
return result.Err
|
||||||
} else {
|
} else {
|
||||||
@@ -381,6 +395,14 @@ func sendNotificationEmail(post *model.Post, user *model.User, channel *model.Ch
|
|||||||
mailTemplate = "api.templates.post_subject_in_direct_message"
|
mailTemplate = "api.templates.post_subject_in_direct_message"
|
||||||
mailParameters = map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName,
|
mailParameters = map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName,
|
||||||
"SenderDisplayName": senderDisplayName, "Month": month, "Day": day, "Year": year}
|
"SenderDisplayName": senderDisplayName, "Month": month, "Day": day, "Year": year}
|
||||||
|
} else if channel.Type == model.CHANNEL_GROUP {
|
||||||
|
bodyText = userLocale("api.post.send_notifications_and_forget.mention_body")
|
||||||
|
|
||||||
|
senderDisplayName := senderName
|
||||||
|
|
||||||
|
mailTemplate = "api.templates.post_subject_in_group_message"
|
||||||
|
mailParameters = map[string]interface{}{"SenderDisplayName": senderDisplayName, "Month": month, "Day": day, "Year": year}
|
||||||
|
channelName = userLocale("api.templates.channel_name.group")
|
||||||
} else {
|
} else {
|
||||||
bodyText = userLocale("api.post.send_notifications_and_forget.mention_body")
|
bodyText = userLocale("api.post.send_notifications_and_forget.mention_body")
|
||||||
subjectText = userLocale("api.post.send_notifications_and_forget.mention_subject")
|
subjectText = userLocale("api.post.send_notifications_and_forget.mention_subject")
|
||||||
@@ -456,18 +478,14 @@ func GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFun
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendPushNotification(post *model.Post, user *model.User, channel *model.Channel, senderName string, wasMentioned bool) *model.AppError {
|
func sendPushNotification(post *model.Post, user *model.User, channel *model.Channel, senderName, channelName string, wasMentioned bool) *model.AppError {
|
||||||
sessions, err := getMobileAppSessions(user.Id)
|
sessions, err := getMobileAppSessions(user.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var channelName string
|
|
||||||
|
|
||||||
if channel.Type == model.CHANNEL_DIRECT {
|
if channel.Type == model.CHANNEL_DIRECT {
|
||||||
channelName = senderName
|
channelName = senderName
|
||||||
} else {
|
|
||||||
channelName = channel.DisplayName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
userLocale := utils.GetUserTranslations(user.Locale)
|
userLocale := utils.GetUserTranslations(user.Locale)
|
||||||
@@ -495,7 +513,7 @@ func sendPushNotification(post *model.Post, user *model.User, channel *model.Cha
|
|||||||
if channel.Type == model.CHANNEL_DIRECT {
|
if channel.Type == model.CHANNEL_DIRECT {
|
||||||
msg.Category = model.CATEGORY_DM
|
msg.Category = model.CATEGORY_DM
|
||||||
msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message")
|
msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message")
|
||||||
} else if wasMentioned {
|
} else if wasMentioned || channel.Type == model.CHANNEL_GROUP {
|
||||||
msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName
|
msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName
|
||||||
} else {
|
} else {
|
||||||
msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention") + channelName
|
msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention") + channelName
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ func LeaveTeam(team *model.Team, user *model.User) *model.AppError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, channel := range *channelList {
|
for _, channel := range *channelList {
|
||||||
if channel.Type != model.CHANNEL_DIRECT {
|
if !channel.IsGroupOrDirect() {
|
||||||
InvalidateCacheForChannelMembers(channel.Id)
|
InvalidateCacheForChannelMembers(channel.Id)
|
||||||
if result := <-Srv.Store.Channel().RemoveMember(channel.Id, user.Id); result.Err != nil {
|
if result := <-Srv.Store.Channel().RemoveMember(channel.Id, user.Id); result.Err != nil {
|
||||||
return result.Err
|
return result.Err
|
||||||
|
|||||||
28
i18n/en.json
28
i18n/en.json
@@ -199,6 +199,14 @@
|
|||||||
"id": "api.channel.create_channel.direct_channel.app_error",
|
"id": "api.channel.create_channel.direct_channel.app_error",
|
||||||
"translation": "Must use createDirectChannel API service for direct message channel creation"
|
"translation": "Must use createDirectChannel API service for direct message channel creation"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.channel.create_group.bad_size.app_error",
|
||||||
|
"translation": "Group message channels must contain at least 3 and no more than 8 users"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.channel.create_group.bad_user.app_error",
|
||||||
|
"translation": "One of the provided users does not exist"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.channel.create_channel.invalid_character.app_error",
|
"id": "api.channel.create_channel.invalid_character.app_error",
|
||||||
"translation": "Invalid character '__' in channel name for non-direct channel"
|
"translation": "Invalid character '__' in channel name for non-direct channel"
|
||||||
@@ -887,6 +895,10 @@
|
|||||||
"id": "api.email_batching.render_batched_post.direct_message",
|
"id": "api.email_batching.render_batched_post.direct_message",
|
||||||
"translation": "Direct Message"
|
"translation": "Direct Message"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.email_batching.render_batched_post.group_message",
|
||||||
|
"translation": "Group Message"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.email_batching.render_batched_post.go_to_post",
|
"id": "api.email_batching.render_batched_post.go_to_post",
|
||||||
"translation": "Go to Post"
|
"translation": "Go to Post"
|
||||||
@@ -2151,6 +2163,14 @@
|
|||||||
"id": "api.templates.post_subject_in_direct_message",
|
"id": "api.templates.post_subject_in_direct_message",
|
||||||
"translation": "{{.SubjectText}} in {{.TeamDisplayName}} from {{.SenderDisplayName}} on {{.Month}} {{.Day}}, {{.Year}}"
|
"translation": "{{.SubjectText}} in {{.TeamDisplayName}} from {{.SenderDisplayName}} on {{.Month}} {{.Day}}, {{.Year}}"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.templates.channel_name.group",
|
||||||
|
"translation": "Group Message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.templates.post_subject_in_group_message",
|
||||||
|
"translation": "New Group Message from {{ .SenderDisplayName}} on {{.Month}} {{.Day}}, {{.Year}}"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.templates.reset_body.button",
|
"id": "api.templates.reset_body.button",
|
||||||
"translation": "Reset Password"
|
"translation": "Reset Password"
|
||||||
@@ -3059,6 +3079,14 @@
|
|||||||
"id": "app.import.import_line.null_post.error",
|
"id": "app.import.import_line.null_post.error",
|
||||||
"translation": "Import data line has type \"post\" but the post object is null."
|
"translation": "Import data line has type \"post\" but the post object is null."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "authentication.permissions.create_group_channel.description",
|
||||||
|
"translation": "Ability to create new group message channels"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "authentication.permissions.create_group_channel.name",
|
||||||
|
"translation": "Create Group Message"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "authentication.permissions.create_team_roles.description",
|
"id": "authentication.permissions.create_team_roles.description",
|
||||||
"translation": "Ability to create new teams"
|
"translation": "Ability to create new teams"
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ var PERMISSION_MANAGE_ROLES *Permission
|
|||||||
var PERMISSION_MANAGE_TEAM_ROLES *Permission
|
var PERMISSION_MANAGE_TEAM_ROLES *Permission
|
||||||
var PERMISSION_MANAGE_CHANNEL_ROLES *Permission
|
var PERMISSION_MANAGE_CHANNEL_ROLES *Permission
|
||||||
var PERMISSION_CREATE_DIRECT_CHANNEL *Permission
|
var PERMISSION_CREATE_DIRECT_CHANNEL *Permission
|
||||||
|
var PERMISSION_CREATE_GROUP_CHANNEL *Permission
|
||||||
var PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES *Permission
|
var PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES *Permission
|
||||||
var PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES *Permission
|
var PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES *Permission
|
||||||
var PERMISSION_LIST_TEAM_CHANNELS *Permission
|
var PERMISSION_LIST_TEAM_CHANNELS *Permission
|
||||||
@@ -149,6 +150,11 @@ func InitalizePermissions() {
|
|||||||
"authentication.permissions.create_direct_channel.name",
|
"authentication.permissions.create_direct_channel.name",
|
||||||
"authentication.permissions.create_direct_channel.description",
|
"authentication.permissions.create_direct_channel.description",
|
||||||
}
|
}
|
||||||
|
PERMISSION_CREATE_GROUP_CHANNEL = &Permission{
|
||||||
|
"create_group_channel",
|
||||||
|
"authentication.permissions.create_group_channel.name",
|
||||||
|
"authentication.permissions.create_group_channel.description",
|
||||||
|
}
|
||||||
PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES = &Permission{
|
PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES = &Permission{
|
||||||
"manage__publicchannel_properties",
|
"manage__publicchannel_properties",
|
||||||
"authentication.permissions.manage_public_channel_properties.name",
|
"authentication.permissions.manage_public_channel_properties.name",
|
||||||
@@ -350,6 +356,7 @@ func InitalizeRoles() {
|
|||||||
"authentication.roles.global_user.description",
|
"authentication.roles.global_user.description",
|
||||||
[]string{
|
[]string{
|
||||||
PERMISSION_CREATE_DIRECT_CHANNEL.Id,
|
PERMISSION_CREATE_DIRECT_CHANNEL.Id,
|
||||||
|
PERMISSION_CREATE_GROUP_CHANNEL.Id,
|
||||||
PERMISSION_PERMANENT_DELETE_USER.Id,
|
PERMISSION_PERMANENT_DELETE_USER.Id,
|
||||||
PERMISSION_MANAGE_OAUTH.Id,
|
PERMISSION_MANAGE_OAUTH.Id,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,8 +4,12 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,6 +17,9 @@ const (
|
|||||||
CHANNEL_OPEN = "O"
|
CHANNEL_OPEN = "O"
|
||||||
CHANNEL_PRIVATE = "P"
|
CHANNEL_PRIVATE = "P"
|
||||||
CHANNEL_DIRECT = "D"
|
CHANNEL_DIRECT = "D"
|
||||||
|
CHANNEL_GROUP = "G"
|
||||||
|
CHANNEL_GROUP_MAX_USERS = 8
|
||||||
|
CHANNEL_GROUP_MIN_USERS = 3
|
||||||
DEFAULT_CHANNEL = "town-square"
|
DEFAULT_CHANNEL = "town-square"
|
||||||
CHANNEL_DISPLAY_NAME_MAX_RUNES = 64
|
CHANNEL_DISPLAY_NAME_MAX_RUNES = 64
|
||||||
CHANNEL_NAME_MIN_LENGTH = 2
|
CHANNEL_NAME_MIN_LENGTH = 2
|
||||||
@@ -89,7 +96,7 @@ func (o *Channel) IsValid() *AppError {
|
|||||||
return NewLocAppError("Channel.IsValid", "model.channel.is_valid.2_or_more.app_error", nil, "id="+o.Id)
|
return NewLocAppError("Channel.IsValid", "model.channel.is_valid.2_or_more.app_error", nil, "id="+o.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !(o.Type == CHANNEL_OPEN || o.Type == CHANNEL_PRIVATE || o.Type == CHANNEL_DIRECT) {
|
if !(o.Type == CHANNEL_OPEN || o.Type == CHANNEL_PRIVATE || o.Type == CHANNEL_DIRECT || o.Type == CHANNEL_GROUP) {
|
||||||
return NewLocAppError("Channel.IsValid", "model.channel.is_valid.type.app_error", nil, "id="+o.Id)
|
return NewLocAppError("Channel.IsValid", "model.channel.is_valid.type.app_error", nil, "id="+o.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +133,10 @@ func (o *Channel) ExtraUpdated() {
|
|||||||
o.ExtraUpdateAt = GetMillis()
|
o.ExtraUpdateAt = GetMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *Channel) IsGroupOrDirect() bool {
|
||||||
|
return o.Type == CHANNEL_DIRECT || o.Type == CHANNEL_GROUP
|
||||||
|
}
|
||||||
|
|
||||||
func GetDMNameFromIds(userId1, userId2 string) string {
|
func GetDMNameFromIds(userId1, userId2 string) string {
|
||||||
if userId1 > userId2 {
|
if userId1 > userId2 {
|
||||||
return userId2 + "__" + userId1
|
return userId2 + "__" + userId1
|
||||||
@@ -133,3 +144,31 @@ func GetDMNameFromIds(userId1, userId2 string) string {
|
|||||||
return userId1 + "__" + userId2
|
return userId1 + "__" + userId2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetGroupDisplayNameFromUsers(users []*User, truncate bool) string {
|
||||||
|
usernames := make([]string, len(users))
|
||||||
|
for index, user := range users {
|
||||||
|
usernames[index] = user.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(usernames)
|
||||||
|
|
||||||
|
name := strings.Join(usernames, ", ")
|
||||||
|
|
||||||
|
if truncate && len(name) > CHANNEL_NAME_MAX_LENGTH {
|
||||||
|
name = name[:CHANNEL_NAME_MAX_LENGTH]
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGroupNameFromUserIds(userIds []string) string {
|
||||||
|
sort.Strings(userIds)
|
||||||
|
|
||||||
|
h := sha1.New()
|
||||||
|
for _, id := range userIds {
|
||||||
|
io.WriteString(h, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
||||||
|
|||||||
@@ -104,3 +104,24 @@ func TestChannelPreUpdate(t *testing.T) {
|
|||||||
o := Channel{Name: "test"}
|
o := Channel{Name: "test"}
|
||||||
o.PreUpdate()
|
o.PreUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetGroupDisplayNameFromUsers(t *testing.T) {
|
||||||
|
users := make([]*User, 4)
|
||||||
|
users[0] = &User{Username: NewId()}
|
||||||
|
users[1] = &User{Username: NewId()}
|
||||||
|
users[2] = &User{Username: NewId()}
|
||||||
|
users[3] = &User{Username: NewId()}
|
||||||
|
|
||||||
|
name := GetGroupDisplayNameFromUsers(users, true)
|
||||||
|
if len(name) > CHANNEL_NAME_MAX_LENGTH {
|
||||||
|
t.Fatal("name too long")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetGroupNameFromUserIds(t *testing.T) {
|
||||||
|
name := GetGroupNameFromUserIds([]string{NewId(), NewId(), NewId(), NewId(), NewId()})
|
||||||
|
|
||||||
|
if len(name) > CHANNEL_NAME_MAX_LENGTH {
|
||||||
|
t.Fatal("name too long")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1121,6 +1121,16 @@ func (c *Client) CreateDirectChannel(userId string) (*Result, *AppError) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) CreateGroupChannel(userIds []string) (*Result, *AppError) {
|
||||||
|
if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/create_group", ArrayToJson(userIds)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
defer closeBody(r)
|
||||||
|
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||||
|
r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) UpdateChannel(channel *Channel) (*Result, *AppError) {
|
func (c *Client) UpdateChannel(channel *Channel) (*Result, *AppError) {
|
||||||
if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/update", channel.ToJson()); err != nil {
|
if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/update", channel.ToJson()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const (
|
|||||||
WEBSOCKET_EVENT_CHANNEL_DELETED = "channel_deleted"
|
WEBSOCKET_EVENT_CHANNEL_DELETED = "channel_deleted"
|
||||||
WEBSOCKET_EVENT_CHANNEL_CREATED = "channel_created"
|
WEBSOCKET_EVENT_CHANNEL_CREATED = "channel_created"
|
||||||
WEBSOCKET_EVENT_DIRECT_ADDED = "direct_added"
|
WEBSOCKET_EVENT_DIRECT_ADDED = "direct_added"
|
||||||
|
WEBSOCKET_EVENT_GROUP_ADDED = "group_added"
|
||||||
WEBSOCKET_EVENT_NEW_USER = "new_user"
|
WEBSOCKET_EVENT_NEW_USER = "new_user"
|
||||||
WEBSOCKET_EVENT_LEAVE_TEAM = "leave_team"
|
WEBSOCKET_EVENT_LEAVE_TEAM = "leave_team"
|
||||||
WEBSOCKET_EVENT_UPDATE_TEAM = "update_team"
|
WEBSOCKET_EVENT_UPDATE_TEAM = "update_team"
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ func (s SqlChannelStore) saveChannelT(transaction *gorp.Transaction, channel *mo
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
if channel.Type != model.CHANNEL_DIRECT {
|
if channel.Type != model.CHANNEL_DIRECT && channel.Type != model.CHANNEL_GROUP {
|
||||||
if count, err := transaction.SelectInt("SELECT COUNT(0) FROM Channels WHERE TeamId = :TeamId AND DeleteAt = 0 AND (Type = 'O' OR Type = 'P')", map[string]interface{}{"TeamId": channel.TeamId}); err != nil {
|
if count, err := transaction.SelectInt("SELECT COUNT(0) FROM Channels WHERE TeamId = :TeamId AND DeleteAt = 0 AND (Type = 'O' OR Type = 'P')", map[string]interface{}{"TeamId": channel.TeamId}); err != nil {
|
||||||
result.Err = model.NewLocAppError("SqlChannelStore.Save", "store.sql_channel.save_channel.current_count.app_error", nil, "teamId="+channel.TeamId+", "+err.Error())
|
result.Err = model.NewLocAppError("SqlChannelStore.Save", "store.sql_channel.save_channel.current_count.app_error", nil, "teamId="+channel.TeamId+", "+err.Error())
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import ChannelStore from 'stores/channel_store.jsx';
|
|||||||
import * as ChannelUtils from 'utils/channel_utils.jsx';
|
import * as ChannelUtils from 'utils/channel_utils.jsx';
|
||||||
import PreferenceStore from 'stores/preference_store.jsx';
|
import PreferenceStore from 'stores/preference_store.jsx';
|
||||||
|
|
||||||
import {loadProfilesAndTeamMembersForDMSidebar} from 'actions/user_actions.jsx';
|
import {loadProfilesForSidebar} from 'actions/user_actions.jsx';
|
||||||
import {trackEvent} from 'actions/diagnostics_actions.jsx';
|
import {trackEvent} from 'actions/diagnostics_actions.jsx';
|
||||||
|
|
||||||
import Client from 'client/web_client.jsx';
|
import Client from 'client/web_client.jsx';
|
||||||
@@ -22,8 +22,12 @@ import {browserHistory} from 'react-router/es6';
|
|||||||
|
|
||||||
export function goToChannel(channel) {
|
export function goToChannel(channel) {
|
||||||
if (channel.fake) {
|
if (channel.fake) {
|
||||||
|
const user = UserStore.getProfileByUsername(channel.display_name);
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
openDirectChannelToUser(
|
openDirectChannelToUser(
|
||||||
UserStore.getProfileByUsername(channel.display_name),
|
user.id,
|
||||||
() => {
|
() => {
|
||||||
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name);
|
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name);
|
||||||
},
|
},
|
||||||
@@ -167,18 +171,18 @@ export function makeUserChannelMember(channelId, userId, success, error) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openDirectChannelToUser(user, success, error) {
|
export function openDirectChannelToUser(userId, success, error) {
|
||||||
const channelName = Utils.getDirectChannelName(UserStore.getCurrentId(), user.id);
|
const channelName = Utils.getDirectChannelName(UserStore.getCurrentId(), userId);
|
||||||
const channel = ChannelStore.getByName(channelName);
|
const channel = ChannelStore.getByName(channelName);
|
||||||
|
|
||||||
if (channel) {
|
if (channel) {
|
||||||
trackEvent('api', 'api_channels_join_direct');
|
trackEvent('api', 'api_channels_join_direct');
|
||||||
PreferenceStore.setPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, user.id, 'true');
|
PreferenceStore.setPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, userId, 'true');
|
||||||
loadProfilesAndTeamMembersForDMSidebar();
|
loadProfilesForSidebar();
|
||||||
|
|
||||||
AsyncClient.savePreference(
|
AsyncClient.savePreference(
|
||||||
Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
|
Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
|
||||||
user.id,
|
userId,
|
||||||
'true'
|
'true'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -190,7 +194,7 @@ export function openDirectChannelToUser(user, success, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Client.createDirectChannel(
|
Client.createDirectChannel(
|
||||||
user.id,
|
userId,
|
||||||
(data) => {
|
(data) => {
|
||||||
Client.getChannel(
|
Client.getChannel(
|
||||||
data.id,
|
data.id,
|
||||||
@@ -201,12 +205,12 @@ export function openDirectChannelToUser(user, success, error) {
|
|||||||
member: data2.member
|
member: data2.member
|
||||||
});
|
});
|
||||||
|
|
||||||
PreferenceStore.setPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, user.id, 'true');
|
PreferenceStore.setPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, userId, 'true');
|
||||||
loadProfilesAndTeamMembersForDMSidebar();
|
loadProfilesForSidebar();
|
||||||
|
|
||||||
AsyncClient.savePreference(
|
AsyncClient.savePreference(
|
||||||
Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
|
Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
|
||||||
user.id,
|
userId,
|
||||||
'true'
|
'true'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -225,6 +229,43 @@ export function openDirectChannelToUser(user, success, error) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openGroupChannelToUsers(userIds, success, error) {
|
||||||
|
Client.createGroupChannel(
|
||||||
|
userIds,
|
||||||
|
(data) => {
|
||||||
|
Client.getChannelMember(
|
||||||
|
data.id,
|
||||||
|
UserStore.getCurrentId(),
|
||||||
|
(data2) => {
|
||||||
|
AppDispatcher.handleServerAction({
|
||||||
|
type: ActionTypes.RECEIVED_CHANNEL,
|
||||||
|
channel: data,
|
||||||
|
member: data2
|
||||||
|
});
|
||||||
|
|
||||||
|
PreferenceStore.setPreference(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, data.id, 'true');
|
||||||
|
loadProfilesForSidebar();
|
||||||
|
|
||||||
|
AsyncClient.savePreference(
|
||||||
|
Preferences.CATEGORY_GROUP_CHANNEL_SHOW,
|
||||||
|
data.id,
|
||||||
|
'true'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
success(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (error) {
|
||||||
|
error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function markFavorite(channelId) {
|
export function markFavorite(channelId) {
|
||||||
trackEvent('api', 'api_channels_favorited');
|
trackEvent('api', 'api_channels_favorited');
|
||||||
AsyncClient.savePreference(Preferences.CATEGORY_FAVORITE_CHANNEL, channelId, 'true');
|
AsyncClient.savePreference(Preferences.CATEGORY_FAVORITE_CHANNEL, channelId, 'true');
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import PreferenceStore from 'stores/preference_store.jsx';
|
|||||||
import SearchStore from 'stores/search_store.jsx';
|
import SearchStore from 'stores/search_store.jsx';
|
||||||
|
|
||||||
import {handleNewPost, loadPosts, loadPostsBefore, loadPostsAfter} from 'actions/post_actions.jsx';
|
import {handleNewPost, loadPosts, loadPostsBefore, loadPostsAfter} from 'actions/post_actions.jsx';
|
||||||
import {loadProfilesAndTeamMembersForDMSidebar} from 'actions/user_actions.jsx';
|
import {loadProfilesForSidebar} from 'actions/user_actions.jsx';
|
||||||
import {loadChannelsForCurrentUser} from 'actions/channel_actions.jsx';
|
import {loadChannelsForCurrentUser} from 'actions/channel_actions.jsx';
|
||||||
import {stopPeriodicStatusUpdates} from 'actions/status_actions.jsx';
|
import {stopPeriodicStatusUpdates} from 'actions/status_actions.jsx';
|
||||||
import * as WebsocketActions from 'actions/websocket_actions.jsx';
|
import * as WebsocketActions from 'actions/websocket_actions.jsx';
|
||||||
@@ -387,7 +387,7 @@ export function emitPreferenceChangedEvent(preference) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (preference.category === Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW) {
|
if (preference.category === Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW) {
|
||||||
loadProfilesAndTeamMembersForDMSidebar();
|
loadProfilesForSidebar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import PostStore from 'stores/post_store.jsx';
|
|||||||
import UserStore from 'stores/user_store.jsx';
|
import UserStore from 'stores/user_store.jsx';
|
||||||
|
|
||||||
import {loadStatusesForChannel} from 'actions/status_actions.jsx';
|
import {loadStatusesForChannel} from 'actions/status_actions.jsx';
|
||||||
import {loadNewDMIfNeeded} from 'actions/user_actions.jsx';
|
import {loadNewDMIfNeeded, loadNewGMIfNeeded} from 'actions/user_actions.jsx';
|
||||||
import {trackEvent} from 'actions/diagnostics_actions.jsx';
|
import {trackEvent} from 'actions/diagnostics_actions.jsx';
|
||||||
|
|
||||||
import Client from 'client/web_client.jsx';
|
import Client from 'client/web_client.jsx';
|
||||||
@@ -24,8 +24,12 @@ export function handleNewPost(post, msg) {
|
|||||||
websocketMessageProps = msg.data;
|
websocketMessageProps = msg.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg && msg.data && msg.data.channel_type === Constants.DM_CHANNEL) {
|
if (msg && msg.data) {
|
||||||
loadNewDMIfNeeded(post.user_id);
|
if (msg.data.channel_type === Constants.DM_CHANNEL) {
|
||||||
|
loadNewDMIfNeeded(post.user_id);
|
||||||
|
} else if (msg.data.channel_type === Constants.GM_CHANNEL) {
|
||||||
|
loadNewGMIfNeeded(post.channel_id, post.user_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (post.root_id && PostStore.getPost(post.channel_id, post.root_id) == null) {
|
if (post.root_id && PostStore.getPost(post.channel_id, post.root_id) == null) {
|
||||||
|
|||||||
@@ -225,63 +225,150 @@ function populateDMChannelsWithProfiles(userIds) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function populateChannelWithProfiles(channelId, userIds) {
|
||||||
|
for (let i = 0; i < userIds.length; i++) {
|
||||||
|
UserStore.saveUserIdInChannel(channelId, userIds[i]);
|
||||||
|
}
|
||||||
|
UserStore.emitInChannelChange();
|
||||||
|
}
|
||||||
|
|
||||||
export function loadNewDMIfNeeded(userId) {
|
export function loadNewDMIfNeeded(userId) {
|
||||||
if (userId === UserStore.getCurrentId()) {
|
if (userId === UserStore.getCurrentId()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pref = PreferenceStore.get(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, userId, 'false');
|
const pref = PreferenceStore.getBool(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, userId, false);
|
||||||
if (pref === 'false') {
|
if (pref === false) {
|
||||||
PreferenceStore.setPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, userId, 'true');
|
PreferenceStore.setPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, userId, 'true');
|
||||||
AsyncClient.savePreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, userId, 'true');
|
AsyncClient.savePreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, userId, 'true');
|
||||||
loadProfilesAndTeamMembersForDMSidebar();
|
loadProfilesForDM();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadProfilesAndTeamMembersForDMSidebar() {
|
export function loadNewGMIfNeeded(channelId, userId) {
|
||||||
const dmPrefs = PreferenceStore.getCategory(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
|
if (userId === UserStore.getCurrentId()) {
|
||||||
const teamId = TeamStore.getCurrentId();
|
return;
|
||||||
const profilesToLoad = [];
|
}
|
||||||
const membersToLoad = [];
|
|
||||||
|
|
||||||
for (const [key, value] of dmPrefs) {
|
function checkPreference() {
|
||||||
if (value === 'true') {
|
const pref = PreferenceStore.getBool(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channelId, false);
|
||||||
if (!UserStore.hasProfile(key)) {
|
if (pref === false) {
|
||||||
profilesToLoad.push(key);
|
PreferenceStore.setPreference(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channelId, 'true');
|
||||||
}
|
AsyncClient.savePreference(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channelId, 'true');
|
||||||
membersToLoad.push(key);
|
loadProfilesForGM();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const channelMembers = ChannelStore.getMyMembers();
|
const channel = ChannelStore.get(channelId);
|
||||||
|
if (channel) {
|
||||||
|
checkPreference();
|
||||||
|
} else {
|
||||||
|
Client.getChannel(
|
||||||
|
channelId,
|
||||||
|
(data) => {
|
||||||
|
AppDispatcher.handleServerAction({
|
||||||
|
type: ActionTypes.RECEIVED_CHANNEL,
|
||||||
|
channel: data.channel,
|
||||||
|
member: data.member
|
||||||
|
});
|
||||||
|
|
||||||
|
checkPreference();
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
AsyncClient.dispatchError(err, 'getChannel');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadProfilesForSidebar() {
|
||||||
|
loadProfilesForDM();
|
||||||
|
loadProfilesForGM();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadProfilesForGM() {
|
||||||
const channels = ChannelStore.getChannels();
|
const channels = ChannelStore.getChannels();
|
||||||
const newPreferences = [];
|
const newPreferences = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < channels.length; i++) {
|
||||||
|
const channel = channels[i];
|
||||||
|
if (channel.type !== Constants.GM_CHANNEL) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UserStore.getProfileListInChannel(channel.id).length >= Constants.MIN_USERS_IN_GM) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isVisible = PreferenceStore.getBool(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channel.id);
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
const member = ChannelStore.getMyMember(channel.id);
|
||||||
|
if (!member || (member.mention_count === 0 && member.msg_count < member.total_msg_count)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
newPreferences.push({
|
||||||
|
user_id: UserStore.getCurrentId(),
|
||||||
|
category: Preferences.CATEGORY_GROUP_CHANNEL_SHOW,
|
||||||
|
name: channel.id,
|
||||||
|
value: 'true'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.getProfilesInChannel(
|
||||||
|
channel.id,
|
||||||
|
0,
|
||||||
|
Constants.MAX_USERS_IN_GM,
|
||||||
|
(data) => {
|
||||||
|
AppDispatcher.handleServerAction({
|
||||||
|
type: ActionTypes.RECEIVED_PROFILES,
|
||||||
|
profiles: data
|
||||||
|
});
|
||||||
|
|
||||||
|
populateChannelWithProfiles(channel.id, Object.keys(data));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPreferences.length > 0) {
|
||||||
|
AsyncClient.savePreferences(newPreferences);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadProfilesForDM() {
|
||||||
|
const channels = ChannelStore.getChannels();
|
||||||
|
const newPreferences = [];
|
||||||
|
const profilesToLoad = [];
|
||||||
|
const profileIds = [];
|
||||||
|
|
||||||
for (let i = 0; i < channels.length; i++) {
|
for (let i = 0; i < channels.length; i++) {
|
||||||
const channel = channels[i];
|
const channel = channels[i];
|
||||||
if (channel.type !== Constants.DM_CHANNEL) {
|
if (channel.type !== Constants.DM_CHANNEL) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const member = channelMembers[channel.id];
|
const teammateId = channel.name.replace(UserStore.getCurrentId(), '').replace('__', '');
|
||||||
if (!member) {
|
const isVisible = PreferenceStore.getBool(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammateId);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const teammateId = channel.name.replace(member.user_id, '').replace('__', '');
|
if (!isVisible) {
|
||||||
|
const member = ChannelStore.getMyMember(channel.id);
|
||||||
|
if (!member || member.mention_count === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (member.mention_count > 0 && membersToLoad.indexOf(teammateId) === -1) {
|
|
||||||
membersToLoad.push(teammateId);
|
|
||||||
newPreferences.push({
|
newPreferences.push({
|
||||||
user_id: UserStore.getCurrentId(),
|
user_id: UserStore.getCurrentId(),
|
||||||
category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
|
category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
|
||||||
name: teammateId,
|
name: teammateId,
|
||||||
value: 'true'
|
value: 'true'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!UserStore.hasProfile(teammateId)) {
|
|
||||||
profilesToLoad.push(teammateId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!UserStore.hasProfile(teammateId)) {
|
||||||
|
profilesToLoad.push(teammateId);
|
||||||
|
}
|
||||||
|
profileIds.push(teammateId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPreferences.length > 0) {
|
if (newPreferences.length > 0) {
|
||||||
@@ -298,44 +385,14 @@ export function loadProfilesAndTeamMembersForDMSidebar() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Use membersToLoad so we get all the DM profiles even if they were already loaded
|
// Use membersToLoad so we get all the DM profiles even if they were already loaded
|
||||||
populateDMChannelsWithProfiles(membersToLoad);
|
populateDMChannelsWithProfiles(profileIds);
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
AsyncClient.dispatchError(err, 'getProfilesByIds');
|
AsyncClient.dispatchError(err, 'getProfilesByIds');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
populateDMChannelsWithProfiles(membersToLoad);
|
populateDMChannelsWithProfiles(profileIds);
|
||||||
}
|
|
||||||
|
|
||||||
if (membersToLoad.length > 0) {
|
|
||||||
Client.getTeamMembersByIds(
|
|
||||||
teamId,
|
|
||||||
membersToLoad,
|
|
||||||
(data) => {
|
|
||||||
const memberMap = {};
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
memberMap[data[i].user_id] = data[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
const nonMembersMap = {};
|
|
||||||
for (let i = 0; i < membersToLoad.length; i++) {
|
|
||||||
if (!memberMap[membersToLoad[i]]) {
|
|
||||||
nonMembersMap[membersToLoad[i]] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppDispatcher.handleServerAction({
|
|
||||||
type: ActionTypes.RECEIVED_MEMBERS_IN_TEAM,
|
|
||||||
team_id: teamId,
|
|
||||||
team_members: memberMap,
|
|
||||||
non_team_members: nonMembersMap
|
|
||||||
});
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
AsyncClient.dispatchError(err, 'getTeamMembersByIds');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -801,3 +858,27 @@ export function uploadProfileImage(userPicture, success, error) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loadProfiles(offset = UserStore.getPagingOffset(), limit = Constants.PROFILE_CHUNK_SIZE, success, error) {
|
||||||
|
Client.getProfiles(
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
(data) => {
|
||||||
|
AppDispatcher.handleServerAction({
|
||||||
|
type: ActionTypes.RECEIVED_PROFILES,
|
||||||
|
profiles: data
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
success(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
AsyncClient.dispatchError(err, 'getProfiles');
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {getSiteURL} from 'utils/url.jsx';
|
|||||||
|
|
||||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||||
import {handleNewPost, loadPosts, loadProfilesForPosts} from 'actions/post_actions.jsx';
|
import {handleNewPost, loadPosts, loadProfilesForPosts} from 'actions/post_actions.jsx';
|
||||||
import {loadProfilesAndTeamMembersForDMSidebar} from 'actions/user_actions.jsx';
|
import {loadProfilesForSidebar} from 'actions/user_actions.jsx';
|
||||||
import {loadChannelsForCurrentUser} from 'actions/channel_actions.jsx';
|
import {loadChannelsForCurrentUser} from 'actions/channel_actions.jsx';
|
||||||
import * as StatusActions from 'actions/status_actions.jsx';
|
import * as StatusActions from 'actions/status_actions.jsx';
|
||||||
|
|
||||||
@@ -244,7 +244,7 @@ function handleUpdateTeamEvent(msg) {
|
|||||||
function handleDirectAddedEvent(msg) {
|
function handleDirectAddedEvent(msg) {
|
||||||
AsyncClient.getChannel(msg.broadcast.channel_id);
|
AsyncClient.getChannel(msg.broadcast.channel_id);
|
||||||
PreferenceStore.setPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, msg.data.teammate_id, 'true');
|
PreferenceStore.setPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, msg.data.teammate_id, 'true');
|
||||||
loadProfilesAndTeamMembersForDMSidebar();
|
loadProfilesForSidebar();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUserAddedEvent(msg) {
|
function handleUserAddedEvent(msg) {
|
||||||
|
|||||||
@@ -1278,6 +1278,16 @@ export default class Client {
|
|||||||
this.trackEvent('api', 'api_channels_create_direct', {team_id: this.getTeamId()});
|
this.trackEvent('api', 'api_channels_create_direct', {team_id: this.getTeamId()});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createGroupChannel(userIds, success, error) {
|
||||||
|
request.
|
||||||
|
post(`${this.getChannelsRoute()}/create_group`).
|
||||||
|
set(this.defaultHeaders).
|
||||||
|
type('application/json').
|
||||||
|
accept('application/json').
|
||||||
|
send(userIds).
|
||||||
|
end(this.handleResponse.bind(this, 'createGroupChannel', success, error));
|
||||||
|
}
|
||||||
|
|
||||||
updateChannel(channel, success, error) {
|
updateChannel(channel, success, error) {
|
||||||
request.
|
request.
|
||||||
post(`${this.getChannelsRoute()}/update`).
|
post(`${this.getChannelsRoute()}/update`).
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ import {getSiteURL} from 'utils/url.jsx';
|
|||||||
import * as TextFormatting from 'utils/text_formatting.jsx';
|
import * as TextFormatting from 'utils/text_formatting.jsx';
|
||||||
import {getFlaggedPosts} from 'actions/post_actions.jsx';
|
import {getFlaggedPosts} from 'actions/post_actions.jsx';
|
||||||
|
|
||||||
import {Constants, Preferences, UserStatuses} from 'utils/constants.jsx';
|
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
|
||||||
|
|
||||||
|
import {Constants, Preferences, UserStatuses, ActionTypes} from 'utils/constants.jsx';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
@@ -53,6 +55,7 @@ export default class ChannelHeader extends React.Component {
|
|||||||
this.getFlagged = this.getFlagged.bind(this);
|
this.getFlagged = this.getFlagged.bind(this);
|
||||||
this.initWebrtc = this.initWebrtc.bind(this);
|
this.initWebrtc = this.initWebrtc.bind(this);
|
||||||
this.onBusy = this.onBusy.bind(this);
|
this.onBusy = this.onBusy.bind(this);
|
||||||
|
this.openDirectMessageModal = this.openDirectMessageModal.bind(this);
|
||||||
|
|
||||||
const state = this.getStateFromStores();
|
const state = this.getStateFromStores();
|
||||||
state.showEditChannelPurposeModal = false;
|
state.showEditChannelPurposeModal = false;
|
||||||
@@ -198,6 +201,14 @@ export default class ChannelHeader extends React.Component {
|
|||||||
this.setState({isBusy});
|
this.setState({isBusy});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openDirectMessageModal() {
|
||||||
|
AppDispatcher.handleViewAction({
|
||||||
|
type: ActionTypes.TOGGLE_DM_MODAL,
|
||||||
|
value: true,
|
||||||
|
startingUsers: UserStore.getProfileListInChannel(this.props.channelId, true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const flagIcon = Constants.FLAG_ICON_SVG;
|
const flagIcon = Constants.FLAG_ICON_SVG;
|
||||||
|
|
||||||
@@ -246,7 +257,8 @@ export default class ChannelHeader extends React.Component {
|
|||||||
const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
|
const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
|
||||||
const isSystemAdmin = UserStore.isSystemAdminForCurrentUser();
|
const isSystemAdmin = UserStore.isSystemAdminForCurrentUser();
|
||||||
const isChannelAdmin = ChannelStore.isChannelAdminForCurrentChannel();
|
const isChannelAdmin = ChannelStore.isChannelAdminForCurrentChannel();
|
||||||
const isDirect = (this.state.channel.type === 'D');
|
const isDirect = (this.state.channel.type === Constants.DM_CHANNEL);
|
||||||
|
const isGroup = (this.state.channel.type === Constants.GM_CHANNEL);
|
||||||
let webrtc;
|
let webrtc;
|
||||||
|
|
||||||
if (isDirect) {
|
if (isDirect) {
|
||||||
@@ -319,6 +331,10 @@ export default class ChannelHeader extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isGroup) {
|
||||||
|
channelTitle = ChannelUtils.buildGroupChannelName(channel.id);
|
||||||
|
}
|
||||||
|
|
||||||
let channelTerm = (
|
let channelTerm = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='channel_header.channel'
|
id='channel_header.channel'
|
||||||
@@ -364,6 +380,64 @@ export default class ChannelHeader extends React.Component {
|
|||||||
</ToggleModalButton>
|
</ToggleModalButton>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
} else if (isGroup) {
|
||||||
|
dropdownContents.push(
|
||||||
|
<li
|
||||||
|
key='edit_header_direct'
|
||||||
|
role='presentation'
|
||||||
|
>
|
||||||
|
<ToggleModalButton
|
||||||
|
role='menuitem'
|
||||||
|
dialogType={EditChannelHeaderModal}
|
||||||
|
dialogProps={{channel}}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_header.channelHeader'
|
||||||
|
defaultMessage='Edit Channel Header'
|
||||||
|
/>
|
||||||
|
</ToggleModalButton>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
dropdownContents.push(
|
||||||
|
<li
|
||||||
|
key='notification_preferences'
|
||||||
|
role='presentation'
|
||||||
|
>
|
||||||
|
<ToggleModalButton
|
||||||
|
role='menuitem'
|
||||||
|
dialogType={ChannelNotificationsModal}
|
||||||
|
dialogProps={{
|
||||||
|
channel,
|
||||||
|
channelMember: this.state.memberChannel,
|
||||||
|
currentUser: this.state.currentUser
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_header.notificationPreferences'
|
||||||
|
defaultMessage='Notification Preferences'
|
||||||
|
/>
|
||||||
|
</ToggleModalButton>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
dropdownContents.push(
|
||||||
|
<li
|
||||||
|
key='add_members'
|
||||||
|
role='presentation'
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
role='menuitem'
|
||||||
|
href='#'
|
||||||
|
onClick={this.openDirectMessageModal}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_header.addMembers'
|
||||||
|
defaultMessage='Add Members'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
dropdownContents.push(
|
dropdownContents.push(
|
||||||
<li
|
<li
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export default class SwitchChannelModal extends React.Component {
|
|||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
openDirectChannelToUser(
|
openDirectChannelToUser(
|
||||||
user,
|
user.id,
|
||||||
(ch) => {
|
(ch) => {
|
||||||
channel = ch;
|
channel = ch;
|
||||||
this.switchToChannel(channel);
|
this.switchToChannel(channel);
|
||||||
@@ -117,7 +117,7 @@ export default class SwitchChannelModal extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel = ChannelStore.getByName(this.selected.name);
|
channel = ChannelStore.get(this.selected.id);
|
||||||
this.switchToChannel(channel);
|
this.switchToChannel(channel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import SearchableUserList from 'components/searchable_user_list.jsx';
|
import MultiSelect from 'components/multiselect/multiselect.jsx';
|
||||||
import SpinnerButton from 'components/spinner_button.jsx';
|
import ProfilePicture from 'components/profile_picture.jsx';
|
||||||
|
|
||||||
import {searchUsers} from 'actions/user_actions.jsx';
|
import {searchUsers} from 'actions/user_actions.jsx';
|
||||||
import {openDirectChannelToUser} from 'actions/channel_actions.jsx';
|
import {openDirectChannelToUser, openGroupChannelToUsers} from 'actions/channel_actions.jsx';
|
||||||
|
|
||||||
import UserStore from 'stores/user_store.jsx';
|
import UserStore from 'stores/user_store.jsx';
|
||||||
import TeamStore from 'stores/team_store.jsx';
|
import TeamStore from 'stores/team_store.jsx';
|
||||||
|
|
||||||
import * as AsyncClient from 'utils/async_client.jsx';
|
import * as AsyncClient from 'utils/async_client.jsx';
|
||||||
import * as UserAgent from 'utils/user_agent.jsx';
|
|
||||||
import {localizeMessage} from 'utils/utils.jsx';
|
|
||||||
import Constants from 'utils/constants.jsx';
|
import Constants from 'utils/constants.jsx';
|
||||||
|
import {displayUsernameForUser} from 'utils/utils.jsx';
|
||||||
|
import Client from 'client/web_client.jsx';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {Modal} from 'react-bootstrap';
|
import {Modal} from 'react-bootstrap';
|
||||||
@@ -21,6 +21,7 @@ import {FormattedMessage} from 'react-intl';
|
|||||||
import {browserHistory} from 'react-router/es6';
|
import {browserHistory} from 'react-router/es6';
|
||||||
|
|
||||||
const USERS_PER_PAGE = 50;
|
const USERS_PER_PAGE = 50;
|
||||||
|
const MAX_SELECTABLE_VALUES = Constants.MAX_USERS_IN_GM - 1;
|
||||||
|
|
||||||
export default class MoreDirectChannels extends React.Component {
|
export default class MoreDirectChannels extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -28,21 +29,31 @@ export default class MoreDirectChannels extends React.Component {
|
|||||||
|
|
||||||
this.handleHide = this.handleHide.bind(this);
|
this.handleHide = this.handleHide.bind(this);
|
||||||
this.handleExit = this.handleExit.bind(this);
|
this.handleExit = this.handleExit.bind(this);
|
||||||
this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
this.handleDelete = this.handleDelete.bind(this);
|
||||||
this.onChange = this.onChange.bind(this);
|
this.onChange = this.onChange.bind(this);
|
||||||
this.createJoinDirectChannelButton = this.createJoinDirectChannelButton.bind(this);
|
|
||||||
this.toggleList = this.toggleList.bind(this);
|
|
||||||
this.nextPage = this.nextPage.bind(this);
|
|
||||||
this.search = this.search.bind(this);
|
this.search = this.search.bind(this);
|
||||||
|
this.addValue = this.addValue.bind(this);
|
||||||
|
|
||||||
this.searchTimeoutId = 0;
|
this.searchTimeoutId = 0;
|
||||||
|
this.listType = global.window.mm_config.RestrictDirectMessage;
|
||||||
|
|
||||||
|
const values = [];
|
||||||
|
if (props.startingUsers) {
|
||||||
|
for (let i = 0; i < props.startingUsers.length; i++) {
|
||||||
|
const user = Object.assign({}, props.startingUsers[i]);
|
||||||
|
user.value = user.id;
|
||||||
|
user.label = '@' + user.username;
|
||||||
|
values.push(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
users: null,
|
users: null,
|
||||||
loadingDMChannel: -1,
|
values,
|
||||||
listType: 'team',
|
|
||||||
show: true,
|
show: true,
|
||||||
search: false
|
search: false,
|
||||||
|
loadingChannel: -1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,17 +61,18 @@ export default class MoreDirectChannels extends React.Component {
|
|||||||
UserStore.addChangeListener(this.onChange);
|
UserStore.addChangeListener(this.onChange);
|
||||||
UserStore.addInTeamChangeListener(this.onChange);
|
UserStore.addInTeamChangeListener(this.onChange);
|
||||||
UserStore.addStatusesChangeListener(this.onChange);
|
UserStore.addStatusesChangeListener(this.onChange);
|
||||||
TeamStore.addChangeListener(this.onChange);
|
|
||||||
|
|
||||||
AsyncClient.getProfiles(0, Constants.PROFILE_CHUNK_SIZE);
|
if (this.listType === 'any') {
|
||||||
AsyncClient.getProfilesInTeam(TeamStore.getCurrentId(), 0, Constants.PROFILE_CHUNK_SIZE);
|
AsyncClient.getProfiles(0, USERS_PER_PAGE * 2);
|
||||||
|
} else {
|
||||||
|
AsyncClient.getProfilesInTeam(TeamStore.getCurrentId(), 0, USERS_PER_PAGE * 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
UserStore.removeChangeListener(this.onChange);
|
UserStore.removeChangeListener(this.onChange);
|
||||||
UserStore.removeInTeamChangeListener(this.onChange);
|
UserStore.removeInTeamChangeListener(this.onChange);
|
||||||
UserStore.removeStatusesChangeListener(this.onChange);
|
UserStore.removeStatusesChangeListener(this.onChange);
|
||||||
TeamStore.removeChangeListener(this.onChange);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleHide() {
|
handleHide() {
|
||||||
@@ -68,8 +80,8 @@ export default class MoreDirectChannels extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleExit() {
|
handleExit() {
|
||||||
if (this.exitToDirectChannel) {
|
if (this.exitToChannel) {
|
||||||
browserHistory.push(this.exitToDirectChannel);
|
browserHistory.push(this.exitToChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.onModalDismissed) {
|
if (this.props.onModalDismissed) {
|
||||||
@@ -77,28 +89,49 @@ export default class MoreDirectChannels extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowDirectChannel(teammate, e) {
|
handleSubmit(e) {
|
||||||
e.preventDefault();
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.state.loadingDMChannel !== -1) {
|
if (this.state.loadingChannel !== -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({loadingDMChannel: teammate.id});
|
const userIds = this.state.values.map((v) => v.id);
|
||||||
openDirectChannelToUser(
|
if (userIds.length === 0) {
|
||||||
teammate,
|
return;
|
||||||
(channel) => {
|
}
|
||||||
// Due to how react-overlays Modal handles focus, we delay pushing
|
|
||||||
// the new channel information until the modal is fully exited.
|
this.setState({loadingChannel: 1});
|
||||||
// The channel information will be pushed in `handleExit`
|
|
||||||
this.exitToDirectChannel = TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name;
|
const success = (channel) => {
|
||||||
this.setState({loadingDMChannel: -1});
|
// Due to how react-overlays Modal handles focus, we delay pushing
|
||||||
this.handleHide();
|
// the new channel information until the modal is fully exited.
|
||||||
},
|
// The channel information will be pushed in `handleExit`
|
||||||
() => {
|
this.exitToChannel = TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name;
|
||||||
this.setState({loadingDMChannel: -1});
|
this.setState({loadingChannel: -1});
|
||||||
}
|
this.handleHide();
|
||||||
);
|
};
|
||||||
|
|
||||||
|
const error = () => {
|
||||||
|
this.setState({loadingChannel: -1});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (userIds.length === 1) {
|
||||||
|
openDirectChannelToUser(userIds[0], success, error);
|
||||||
|
} else {
|
||||||
|
openGroupChannelToUsers(userIds, success, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addValue(value) {
|
||||||
|
const values = Object.assign([], this.state.values);
|
||||||
|
if (values.indexOf(value) === -1) {
|
||||||
|
values.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({values});
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(force) {
|
onChange(force) {
|
||||||
@@ -107,10 +140,17 @@ export default class MoreDirectChannels extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let users;
|
let users;
|
||||||
if (this.state.listType === 'any') {
|
if (this.listType === 'any') {
|
||||||
users = UserStore.getProfileList(true);
|
users = Object.assign([], UserStore.getProfileList(true));
|
||||||
} else {
|
} else {
|
||||||
users = UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true, true);
|
users = Object.assign([], UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < users.length; i++) {
|
||||||
|
const user = Object.assign({}, users[i]);
|
||||||
|
user.value = user.id;
|
||||||
|
user.label = '@' + user.username;
|
||||||
|
users[i] = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -118,72 +158,51 @@ export default class MoreDirectChannels extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleList(e) {
|
handlePageChange(page, prevPage) {
|
||||||
const listType = e.target.value;
|
if (page > prevPage) {
|
||||||
let users;
|
|
||||||
if (listType === 'any') {
|
|
||||||
users = UserStore.getProfileList(true);
|
|
||||||
} else {
|
|
||||||
users = UserStore.getProfileListInTeam(TeamStore.getCurrentId(), true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
users,
|
|
||||||
listType
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createJoinDirectChannelButton({user}) {
|
|
||||||
return (
|
|
||||||
<SpinnerButton
|
|
||||||
className='btn btm-sm btn-primary'
|
|
||||||
spinning={this.state.loadingDMChannel === user.id}
|
|
||||||
onClick={this.handleShowDirectChannel.bind(this, user)}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id='more_direct_channels.message'
|
|
||||||
defaultMessage='Message'
|
|
||||||
/>
|
|
||||||
</SpinnerButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
nextPage(page) {
|
|
||||||
if (this.state.listType === 'any') {
|
|
||||||
AsyncClient.getProfiles((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE);
|
AsyncClient.getProfiles((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE);
|
||||||
} else {
|
|
||||||
AsyncClient.getProfilesInTeam(TeamStore.getCurrentId(), (page + 1) * USERS_PER_PAGE, USERS_PER_PAGE);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
search(term) {
|
search(term) {
|
||||||
|
clearTimeout(this.searchTimeoutId);
|
||||||
|
|
||||||
if (term === '') {
|
if (term === '') {
|
||||||
this.onChange(true);
|
this.onChange(true);
|
||||||
this.setState({search: false});
|
this.setState({search: false});
|
||||||
|
this.searchTimeoutId = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let teamId;
|
let teamId;
|
||||||
if (this.state.listType === 'any') {
|
if (this.listType === 'any') {
|
||||||
teamId = '';
|
teamId = '';
|
||||||
} else {
|
} else {
|
||||||
teamId = TeamStore.getCurrentId();
|
teamId = TeamStore.getCurrentId();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearTimeout(this.searchTimeoutId);
|
const searchTimeoutId = setTimeout(
|
||||||
|
|
||||||
this.searchTimeoutId = setTimeout(
|
|
||||||
() => {
|
() => {
|
||||||
searchUsers(
|
searchUsers(
|
||||||
term,
|
term,
|
||||||
teamId,
|
teamId,
|
||||||
{},
|
{},
|
||||||
(users) => {
|
(users) => {
|
||||||
|
if (searchTimeoutId !== this.searchTimeoutId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let indexToDelete = -1;
|
||||||
for (let i = 0; i < users.length; i++) {
|
for (let i = 0; i < users.length; i++) {
|
||||||
if (users[i].id === UserStore.getCurrentId()) {
|
if (users[i].id === UserStore.getCurrentId()) {
|
||||||
users.splice(i, 1);
|
indexToDelete = i;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
users[i].value = users[i].id;
|
||||||
|
users[i].label = '@' + users[i].username;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexToDelete !== -1) {
|
||||||
|
users.splice(indexToDelete, 1);
|
||||||
}
|
}
|
||||||
this.setState({search: true, users});
|
this.setState({search: true, users});
|
||||||
}
|
}
|
||||||
@@ -191,44 +210,88 @@ export default class MoreDirectChannels extends React.Component {
|
|||||||
},
|
},
|
||||||
Constants.SEARCH_TIMEOUT_MILLISECONDS
|
Constants.SEARCH_TIMEOUT_MILLISECONDS
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.searchTimeoutId = searchTimeoutId;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
handleDelete(values) {
|
||||||
let teamToggle;
|
this.setState({values});
|
||||||
let memberClass = '';
|
}
|
||||||
if (global.window.mm_config.RestrictDirectMessage === 'any') {
|
|
||||||
memberClass = 'more-system-members';
|
renderOption(option, isSelected, onAdd) {
|
||||||
teamToggle = (
|
var rowSelected = '';
|
||||||
<div className='member-select__container'>
|
if (isSelected) {
|
||||||
<select
|
rowSelected = 'more-modal__row--selected';
|
||||||
className='form-control'
|
|
||||||
id='restrictList'
|
|
||||||
ref='restrictList'
|
|
||||||
defaultValue='team'
|
|
||||||
onChange={this.toggleList}
|
|
||||||
>
|
|
||||||
<option value='any'>
|
|
||||||
{localizeMessage('filtered_user_list.any_team', 'All Users')}
|
|
||||||
</option>
|
|
||||||
<option value='team'>
|
|
||||||
{localizeMessage('filtered_user_list.team_only', 'Members of this Team')}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<span
|
|
||||||
className='member-show'
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id='filtered_user_list.show'
|
|
||||||
defaultMessage='Filter:'
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={option.id}
|
||||||
|
ref={isSelected ? 'selected' : option.id}
|
||||||
|
className={'more-modal__row clickable ' + rowSelected}
|
||||||
|
onClick={() => onAdd(option)}
|
||||||
|
>
|
||||||
|
<ProfilePicture
|
||||||
|
src={`${Client.getUsersRoute()}/${option.id}/image?time=${option.last_picture_update}`}
|
||||||
|
width='32'
|
||||||
|
height='32'
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className='more-modal__details'
|
||||||
|
>
|
||||||
|
<div className='more-modal__name'>
|
||||||
|
{displayUsernameForUser(option)}
|
||||||
|
</div>
|
||||||
|
<div className='more-modal__description'>
|
||||||
|
{option.email}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='more-modal__actions'>
|
||||||
|
<div className='more-modal__actions--round'>
|
||||||
|
<i className='fa fa-plus'/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderValue(user) {
|
||||||
|
return user.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let note;
|
||||||
|
if (this.props.startingUsers) {
|
||||||
|
if (this.state.values && this.state.values.length >= MAX_SELECTABLE_VALUES) {
|
||||||
|
note = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='more_direct_channels.new_convo_note.full'
|
||||||
|
defaultMessage='You’ve reached the maximum number of people for this conversation. Consider creating a private group instead.'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
note = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='more_direct_channels.new_convo_note'
|
||||||
|
defaultMessage='This will start a new conversation. If you’re adding a lot of people, consider creating a private group instead.'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const numRemainingText = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='multiselect.numPeopleRemaining'
|
||||||
|
defaultMessage='You can add {num, number} more {num, plural, =0 {people} one {person} other {people}}. '
|
||||||
|
values={{
|
||||||
|
num: MAX_SELECTABLE_VALUES - this.state.values.length
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
dialogClassName={'more-modal more-direct-channels ' + memberClass}
|
dialogClassName={'more-modal more-direct-channels'}
|
||||||
show={this.state.show}
|
show={this.state.show}
|
||||||
onHide={this.handleHide}
|
onHide={this.handleHide}
|
||||||
onExited={this.handleExit}
|
onExited={this.handleExit}
|
||||||
@@ -242,15 +305,21 @@ export default class MoreDirectChannels extends React.Component {
|
|||||||
</Modal.Title>
|
</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
{teamToggle}
|
<MultiSelect
|
||||||
<SearchableUserList
|
key='moreDirectChannelsList'
|
||||||
key={'moreDirectChannelsList_' + this.state.listType}
|
options={this.state.users}
|
||||||
users={this.state.users}
|
optionRenderer={this.renderOption}
|
||||||
usersPerPage={USERS_PER_PAGE}
|
values={this.state.values}
|
||||||
nextPage={this.nextPage}
|
valueRenderer={this.renderValue}
|
||||||
search={this.search}
|
perPage={USERS_PER_PAGE}
|
||||||
actions={[this.createJoinDirectChannelButton]}
|
handlePageChange={this.handlePageChange}
|
||||||
focusOnMount={!UserAgent.isMobile()}
|
handleInput={this.search}
|
||||||
|
handleDelete={this.handleDelete}
|
||||||
|
handleAdd={this.addValue}
|
||||||
|
handleSubmit={this.handleSubmit}
|
||||||
|
noteText={note}
|
||||||
|
maxValues={MAX_SELECTABLE_VALUES}
|
||||||
|
numRemainingText={numRemainingText}
|
||||||
/>
|
/>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -259,5 +328,6 @@ export default class MoreDirectChannels extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MoreDirectChannels.propTypes = {
|
MoreDirectChannels.propTypes = {
|
||||||
|
startingUsers: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||||
onModalDismissed: React.PropTypes.func
|
onModalDismissed: React.PropTypes.func
|
||||||
};
|
};
|
||||||
257
webapp/components/multiselect/multiselect.jsx
Обычный файл
257
webapp/components/multiselect/multiselect.jsx
Обычный файл
@@ -0,0 +1,257 @@
|
|||||||
|
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import MultiSelectList from './multiselect_list.jsx';
|
||||||
|
|
||||||
|
import {localizeMessage} from 'utils/utils.jsx';
|
||||||
|
import Constants from 'utils/constants.jsx';
|
||||||
|
const KeyCodes = Constants.KeyCodes;
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactSelect from 'react-select';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
|
export default class MultiSelect extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.onChange = this.onChange.bind(this);
|
||||||
|
this.onSelect = this.onSelect.bind(this);
|
||||||
|
this.onAdd = this.onAdd.bind(this);
|
||||||
|
this.onInput = this.onInput.bind(this);
|
||||||
|
this.handleEnterPress = this.handleEnterPress.bind(this);
|
||||||
|
this.nextPage = this.nextPage.bind(this);
|
||||||
|
this.prevPage = this.prevPage.bind(this);
|
||||||
|
|
||||||
|
this.selected = null;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
page: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.addEventListener('keydown', this.handleEnterPress);
|
||||||
|
this.refs.select.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener('keydown', this.handleEnterPress);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPage() {
|
||||||
|
if (this.props.handlePageChange) {
|
||||||
|
this.props.handlePageChange(this.state.page + 1, this.state.page);
|
||||||
|
}
|
||||||
|
this.refs.list.setSelected(0);
|
||||||
|
this.setState({page: this.state.page + 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
prevPage() {
|
||||||
|
if (this.state.page === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.handlePageChange) {
|
||||||
|
this.props.handlePageChange(this.state.page - 1, this.state.page);
|
||||||
|
}
|
||||||
|
this.refs.list.setSelected(0);
|
||||||
|
this.setState({page: this.state.page - 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect(selected) {
|
||||||
|
this.selected = selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
onAdd(value) {
|
||||||
|
if (this.props.maxValues && this.props.values.length >= this.props.maxValues) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < this.props.values.length; i++) {
|
||||||
|
if (this.props.values[i].value === value.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.handleAdd(value);
|
||||||
|
this.selected = null;
|
||||||
|
this.refs.select.handleInputChange({target: {value: ''}});
|
||||||
|
this.onInput('');
|
||||||
|
this.refs.select.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
onInput(input) {
|
||||||
|
if (input === '') {
|
||||||
|
this.refs.list.setSelected(-1);
|
||||||
|
} else {
|
||||||
|
this.refs.list.setSelected(0);
|
||||||
|
}
|
||||||
|
this.selected = null;
|
||||||
|
|
||||||
|
this.props.handleInput(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEnterPress(e) {
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case KeyCodes.ENTER:
|
||||||
|
if (this.selected == null) {
|
||||||
|
this.props.handleSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.onAdd(this.selected);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(values) {
|
||||||
|
if (values.length < this.props.values.length) {
|
||||||
|
this.props.handleDelete(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const options = this.props.options;
|
||||||
|
|
||||||
|
let numRemainingText;
|
||||||
|
if (this.props.numRemainingText) {
|
||||||
|
numRemainingText = this.props.numRemainingText;
|
||||||
|
} else if (this.props.maxValues != null) {
|
||||||
|
numRemainingText = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='multiselect.numRemaining'
|
||||||
|
defaultMessage='You can add {num, number} more. '
|
||||||
|
values={{
|
||||||
|
num: this.props.maxValues - this.props.values.length
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let optionsToDisplay = [];
|
||||||
|
let nextButton;
|
||||||
|
let previousButton;
|
||||||
|
let noteTextContainer;
|
||||||
|
|
||||||
|
if (this.props.noteText) {
|
||||||
|
noteTextContainer = (
|
||||||
|
<div className='multi-select__note'>
|
||||||
|
<div className='note__icon'><span className='fa fa-info'/></div>
|
||||||
|
<div>{this.props.noteText}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options && options.length > this.props.perPage) {
|
||||||
|
const pageStart = this.state.page * this.props.perPage;
|
||||||
|
const pageEnd = pageStart + this.props.perPage;
|
||||||
|
optionsToDisplay = options.slice(pageStart, pageEnd);
|
||||||
|
|
||||||
|
if (options.length > pageEnd) {
|
||||||
|
nextButton = (
|
||||||
|
<button
|
||||||
|
className='btn btn-default filter-control filter-control__next'
|
||||||
|
onClick={this.nextPage}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='filtered_user_list.next'
|
||||||
|
defaultMessage='Next'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.page > 0) {
|
||||||
|
previousButton = (
|
||||||
|
<button
|
||||||
|
className='btn btn-default filter-control filter-control__prev'
|
||||||
|
onClick={this.prevPage}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='filtered_user_list.prev'
|
||||||
|
defaultMessage='Previous'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
optionsToDisplay = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='filtered-user-list'>
|
||||||
|
<div className='filter-row filter-row--full'>
|
||||||
|
<div className='multi-select__container'>
|
||||||
|
<ReactSelect
|
||||||
|
ref='select'
|
||||||
|
multi={true}
|
||||||
|
options={this.props.options}
|
||||||
|
joinValues={true}
|
||||||
|
clearable={false}
|
||||||
|
openOnFocus={true}
|
||||||
|
onInputChange={this.onInput}
|
||||||
|
onBlurResetsInput={false}
|
||||||
|
onCloseResetsInput={false}
|
||||||
|
onChange={this.onChange}
|
||||||
|
value={this.props.values}
|
||||||
|
valueRenderer={this.props.valueRenderer}
|
||||||
|
menuRenderer={() => null}
|
||||||
|
arrowRenderer={() => null}
|
||||||
|
noResultsText={null}
|
||||||
|
placeholder={localizeMessage('multiselect.placeholder', 'Search and add members')}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className='btn btn-primary btn-sm'
|
||||||
|
onClick={this.props.handleSubmit}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='multiselect.go'
|
||||||
|
defaultMessage='Go'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className='multi-select__help'>
|
||||||
|
<div className='hidden-xs'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='multiselect.instructions'
|
||||||
|
defaultMessage='Use up/down arrows to navigate and enter to select'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{numRemainingText}
|
||||||
|
{noteTextContainer}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<MultiSelectList
|
||||||
|
ref='list'
|
||||||
|
options={optionsToDisplay}
|
||||||
|
optionRenderer={this.props.optionRenderer}
|
||||||
|
page={this.state.page}
|
||||||
|
perPage={this.props.perPage}
|
||||||
|
onPageChange={this.props.handlePageChange}
|
||||||
|
onAdd={this.onAdd}
|
||||||
|
onSelect={this.onSelect}
|
||||||
|
/>
|
||||||
|
<div className='filter-controls'>
|
||||||
|
{previousButton}
|
||||||
|
{nextButton}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiSelect.propTypes = {
|
||||||
|
options: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||||
|
optionRenderer: React.PropTypes.func,
|
||||||
|
values: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||||
|
valueRenderer: React.PropTypes.func,
|
||||||
|
handleInput: React.PropTypes.func,
|
||||||
|
handleDelete: React.PropTypes.func,
|
||||||
|
perPage: React.PropTypes.number,
|
||||||
|
handlePageChange: React.PropTypes.func,
|
||||||
|
handleAdd: React.PropTypes.func,
|
||||||
|
handleSubmit: React.PropTypes.func,
|
||||||
|
noteText: React.PropTypes.node,
|
||||||
|
maxValues: React.PropTypes.number,
|
||||||
|
numRemainingText: React.PropTypes.node
|
||||||
|
};
|
||||||
169
webapp/components/multiselect/multiselect_list.jsx
Обычный файл
169
webapp/components/multiselect/multiselect_list.jsx
Обычный файл
@@ -0,0 +1,169 @@
|
|||||||
|
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import {cmdOrCtrlPressed} from 'utils/utils.jsx';
|
||||||
|
import Constants from 'utils/constants.jsx';
|
||||||
|
const KeyCodes = Constants.KeyCodes;
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
|
export default class MultiSelectList extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.defaultOptionRenderer = this.defaultOptionRenderer.bind(this);
|
||||||
|
this.handleArrowPress = this.handleArrowPress.bind(this);
|
||||||
|
this.setSelected = this.setSelected.bind(this);
|
||||||
|
|
||||||
|
this.toSelect = -1;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
selected: -1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.addEventListener('keydown', this.handleArrowPress);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener('keydown', this.handleArrowPress);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this.setState({selected: this.toSelect});
|
||||||
|
|
||||||
|
const options = nextProps.options;
|
||||||
|
|
||||||
|
if (options && options.length > 0 && this.toSelect >= 0) {
|
||||||
|
this.props.onSelect(options[this.toSelect]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
if (this.refs.list && this.refs.selected) {
|
||||||
|
const elemTop = this.refs.selected.getBoundingClientRect().top;
|
||||||
|
const elemBottom = this.refs.selected.getBoundingClientRect().bottom;
|
||||||
|
const listTop = this.refs.list.getBoundingClientRect().top;
|
||||||
|
const listBottom = this.refs.list.getBoundingClientRect().bottom;
|
||||||
|
if (elemBottom > listBottom) {
|
||||||
|
this.refs.selected.scrollIntoView(false);
|
||||||
|
} else if (elemTop < listTop) {
|
||||||
|
this.refs.selected.scrollIntoView(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelected(selected) {
|
||||||
|
this.toSelect = selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleArrowPress(e) {
|
||||||
|
if (cmdOrCtrlPressed(e) && e.shiftKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = this.props.options;
|
||||||
|
if (options.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let selected;
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case KeyCodes.DOWN:
|
||||||
|
if (this.state.selected === -1) {
|
||||||
|
selected = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
selected = Math.min(this.state.selected + 1, options.length - 1);
|
||||||
|
break;
|
||||||
|
case KeyCodes.UP:
|
||||||
|
if (this.state.selected === -1) {
|
||||||
|
selected = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
selected = Math.max(this.state.selected - 1, 0);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
this.setState({selected});
|
||||||
|
this.props.onSelect(options[selected]);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultOptionRenderer(option, isSelected, onAdd) {
|
||||||
|
var rowSelected = '';
|
||||||
|
if (isSelected) {
|
||||||
|
rowSelected = 'more-modal__row--selected';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={isSelected ? 'selected' : option.value}
|
||||||
|
className={rowSelected}
|
||||||
|
key={'multiselectoption' + option.value}
|
||||||
|
onClick={() => onAdd(option)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const options = this.props.options;
|
||||||
|
|
||||||
|
if (options == null || options.length === 0) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key='no-users-found'
|
||||||
|
className='no-channel-message'
|
||||||
|
>
|
||||||
|
<p className='primary-message'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='multiselect.list.notFound'
|
||||||
|
defaultMessage='No items found'
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let renderer;
|
||||||
|
if (this.props.optionRenderer) {
|
||||||
|
renderer = this.props.optionRenderer;
|
||||||
|
} else {
|
||||||
|
renderer = this.defaultOptionRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionControls = options.map((o, i) => renderer(o, this.state.selected === i, this.props.onAdd));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='more-modal__list'>
|
||||||
|
<div
|
||||||
|
ref='list'
|
||||||
|
>
|
||||||
|
{optionControls}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiSelectList.defaultProps = {
|
||||||
|
options: [],
|
||||||
|
perPage: 50,
|
||||||
|
onAction: () => null
|
||||||
|
};
|
||||||
|
|
||||||
|
MultiSelectList.propTypes = {
|
||||||
|
options: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||||
|
optionRenderer: React.PropTypes.func,
|
||||||
|
page: React.PropTypes.number,
|
||||||
|
perPage: React.PropTypes.number,
|
||||||
|
onPageChange: React.PropTypes.func,
|
||||||
|
onAdd: React.PropTypes.func,
|
||||||
|
onSelect: React.PropTypes.func
|
||||||
|
};
|
||||||
@@ -61,6 +61,8 @@ export default class Navbar extends React.Component {
|
|||||||
this.showChannelSwitchModal = this.showChannelSwitchModal.bind(this);
|
this.showChannelSwitchModal = this.showChannelSwitchModal.bind(this);
|
||||||
this.hideChannelSwitchModal = this.hideChannelSwitchModal.bind(this);
|
this.hideChannelSwitchModal = this.hideChannelSwitchModal.bind(this);
|
||||||
|
|
||||||
|
this.openDirectMessageModal = this.openDirectMessageModal.bind(this);
|
||||||
|
|
||||||
const state = this.getStateFromStores();
|
const state = this.getStateFromStores();
|
||||||
state.showEditChannelPurposeModal = false;
|
state.showEditChannelPurposeModal = false;
|
||||||
state.showEditChannelHeaderModal = false;
|
state.showEditChannelHeaderModal = false;
|
||||||
@@ -206,6 +208,14 @@ export default class Navbar extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openDirectMessageModal() {
|
||||||
|
AppDispatcher.handleViewAction({
|
||||||
|
type: ActionTypes.TOGGLE_DM_MODAL,
|
||||||
|
value: true,
|
||||||
|
startingUsers: UserStore.getProfileListInChannel(this.state.channel.id, true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
toggleFavorite = (e) => {
|
toggleFavorite = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -216,7 +226,7 @@ export default class Navbar extends React.Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
createDropdown(channel, channelTitle, isAdmin, isSystemAdmin, isChannelAdmin, isDirect, popoverContent) {
|
createDropdown(channel, channelTitle, isAdmin, isSystemAdmin, isChannelAdmin, isDirect, isGroup, popoverContent) {
|
||||||
if (channel) {
|
if (channel) {
|
||||||
let channelTerm = (
|
let channelTerm = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@@ -258,6 +268,57 @@ export default class Navbar extends React.Component {
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
} else if (isGroup) {
|
||||||
|
setChannelHeaderOption = (
|
||||||
|
<li role='presentation'>
|
||||||
|
<a
|
||||||
|
role='menuitem'
|
||||||
|
href='#'
|
||||||
|
onClick={this.showEditChannelHeaderModal}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='channel_header.channelHeader'
|
||||||
|
defaultMessage='Set Channel Header...'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
notificationPreferenceOption = (
|
||||||
|
<li role='presentation'>
|
||||||
|
<ToggleModalButton
|
||||||
|
role='menuitem'
|
||||||
|
dialogType={ChannelNotificationsModal}
|
||||||
|
dialogProps={{
|
||||||
|
channel,
|
||||||
|
channelMember: this.state.member,
|
||||||
|
currentUser: this.state.currentUser
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='navbar.preferences'
|
||||||
|
defaultMessage='Notification Preferences'
|
||||||
|
/>
|
||||||
|
</ToggleModalButton>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
addMembersOption = (
|
||||||
|
<li
|
||||||
|
role='presentation'
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
role='menuitem'
|
||||||
|
href='#'
|
||||||
|
onClick={this.openDirectMessageModal}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='navbar.addMembers'
|
||||||
|
defaultMessage='Add Members'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
viewInfoOption = (
|
viewInfoOption = (
|
||||||
<li role='presentation'>
|
<li role='presentation'>
|
||||||
@@ -621,6 +682,7 @@ export default class Navbar extends React.Component {
|
|||||||
var isSystemAdmin = false;
|
var isSystemAdmin = false;
|
||||||
var isChannelAdmin = false;
|
var isChannelAdmin = false;
|
||||||
var isDirect = false;
|
var isDirect = false;
|
||||||
|
let isGroup = false;
|
||||||
|
|
||||||
var editChannelHeaderModal = null;
|
var editChannelHeaderModal = null;
|
||||||
var editChannelPurposeModal = null;
|
var editChannelPurposeModal = null;
|
||||||
@@ -660,6 +722,9 @@ export default class Navbar extends React.Component {
|
|||||||
isDirect = true;
|
isDirect = true;
|
||||||
const teammateId = Utils.getUserIdFromChannelName(channel);
|
const teammateId = Utils.getUserIdFromChannelName(channel);
|
||||||
channelTitle = Utils.displayUsername(teammateId);
|
channelTitle = Utils.displayUsername(teammateId);
|
||||||
|
} else if (channel.type === Constants.GM_CHANNEL) {
|
||||||
|
isGroup = true;
|
||||||
|
channelTitle = ChannelUtils.buildGroupChannelName(channel.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channel.header.length === 0) {
|
if (channel.header.length === 0) {
|
||||||
@@ -757,7 +822,7 @@ export default class Navbar extends React.Component {
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
var channelMenuDropdown = this.createDropdown(channel, channelTitle, isAdmin, isSystemAdmin, isChannelAdmin, isDirect, popoverContent);
|
var channelMenuDropdown = this.createDropdown(channel, channelTitle, isAdmin, isSystemAdmin, isChannelAdmin, isDirect, isGroup, popoverContent);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default class PopoverListMembers extends React.Component {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
openDirectChannelToUser(
|
openDirectChannelToUser(
|
||||||
teammate,
|
teammate.id,
|
||||||
(channel, channelAlreadyExisted) => {
|
(channel, channelAlreadyExisted) => {
|
||||||
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name);
|
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name);
|
||||||
if (channelAlreadyExisted) {
|
if (channelAlreadyExisted) {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export default class ProfilePopover extends React.Component {
|
|||||||
this.setState({loadingDMChannel: user.id});
|
this.setState({loadingDMChannel: user.id});
|
||||||
|
|
||||||
openDirectChannelToUser(
|
openDirectChannelToUser(
|
||||||
user,
|
user.id,
|
||||||
(channel) => {
|
(channel) => {
|
||||||
if (Utils.isMobile()) {
|
if (Utils.isMobile()) {
|
||||||
GlobalActions.emitCloseRightHandSide();
|
GlobalActions.emitCloseRightHandSide();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import ChannelStore from 'stores/channel_store.jsx';
|
|||||||
import UserStore from 'stores/user_store.jsx';
|
import UserStore from 'stores/user_store.jsx';
|
||||||
import TeamStore from 'stores/team_store.jsx';
|
import TeamStore from 'stores/team_store.jsx';
|
||||||
import PreferenceStore from 'stores/preference_store.jsx';
|
import PreferenceStore from 'stores/preference_store.jsx';
|
||||||
|
import ModalStore from 'stores/modal_store.jsx';
|
||||||
|
|
||||||
import * as AsyncClient from 'utils/async_client.jsx';
|
import * as AsyncClient from 'utils/async_client.jsx';
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
@@ -22,7 +23,7 @@ import * as ChannelUtils from 'utils/channel_utils.jsx';
|
|||||||
import * as ChannelActions from 'actions/channel_actions.jsx';
|
import * as ChannelActions from 'actions/channel_actions.jsx';
|
||||||
|
|
||||||
import {trackEvent} from 'actions/diagnostics_actions.jsx';
|
import {trackEvent} from 'actions/diagnostics_actions.jsx';
|
||||||
import Constants from 'utils/constants.jsx';
|
import {ActionTypes, Constants} from 'utils/constants.jsx';
|
||||||
|
|
||||||
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
|
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
|
||||||
|
|
||||||
@@ -49,6 +50,8 @@ export default class Sidebar extends React.Component {
|
|||||||
this.getStateFromStores = this.getStateFromStores.bind(this);
|
this.getStateFromStores = this.getStateFromStores.bind(this);
|
||||||
|
|
||||||
this.onChange = this.onChange.bind(this);
|
this.onChange = this.onChange.bind(this);
|
||||||
|
this.onModalChange = this.onModalChange.bind(this);
|
||||||
|
this.onInChannelChange = this.onInChannelChange.bind(this);
|
||||||
this.onScroll = this.onScroll.bind(this);
|
this.onScroll = this.onScroll.bind(this);
|
||||||
this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this);
|
this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this);
|
||||||
this.handleLeaveDirectChannel = this.handleLeaveDirectChannel.bind(this);
|
this.handleLeaveDirectChannel = this.handleLeaveDirectChannel.bind(this);
|
||||||
@@ -77,6 +80,7 @@ export default class Sidebar extends React.Component {
|
|||||||
state.showDirectChannelsModal = false;
|
state.showDirectChannelsModal = false;
|
||||||
state.showMoreChannelsModal = false;
|
state.showMoreChannelsModal = false;
|
||||||
state.loadingDMChannel = -1;
|
state.loadingDMChannel = -1;
|
||||||
|
state.inChannelChange = false;
|
||||||
this.state = state;
|
this.state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +100,7 @@ export default class Sidebar extends React.Component {
|
|||||||
Object.keys(unreadCounts).forEach((chId) => {
|
Object.keys(unreadCounts).forEach((chId) => {
|
||||||
const channel = ChannelStore.get(chId);
|
const channel = ChannelStore.get(chId);
|
||||||
|
|
||||||
if (channel && (channel.type === 'D' || channel.team_id === this.state.currentTeam.id)) {
|
if (channel && (channel.type === Constants.DM_CHANNEL || channel.type === Constants.GM_CHANNEL || channel.team_id === this.state.currentTeam.id)) {
|
||||||
msgs += unreadCounts[chId].msgs;
|
msgs += unreadCounts[chId].msgs;
|
||||||
mentions += unreadCounts[chId].mentions;
|
mentions += unreadCounts[chId].mentions;
|
||||||
}
|
}
|
||||||
@@ -128,13 +132,19 @@ export default class Sidebar extends React.Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onInChannelChange() {
|
||||||
|
this.setState({inChannelChange: !this.state.inChannelChange});
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
ChannelStore.addChangeListener(this.onChange);
|
ChannelStore.addChangeListener(this.onChange);
|
||||||
UserStore.addChangeListener(this.onChange);
|
UserStore.addChangeListener(this.onChange);
|
||||||
UserStore.addInTeamChangeListener(this.onChange);
|
UserStore.addInTeamChangeListener(this.onChange);
|
||||||
|
UserStore.addInChannelChangeListener(this.onInChannelChange);
|
||||||
UserStore.addStatusesChangeListener(this.onChange);
|
UserStore.addStatusesChangeListener(this.onChange);
|
||||||
TeamStore.addChangeListener(this.onChange);
|
TeamStore.addChangeListener(this.onChange);
|
||||||
PreferenceStore.addChangeListener(this.onChange);
|
PreferenceStore.addChangeListener(this.onChange);
|
||||||
|
ModalStore.addModalListener(ActionTypes.TOGGLE_DM_MODAL, this.onModalChange);
|
||||||
|
|
||||||
this.updateTitle();
|
this.updateTitle();
|
||||||
this.updateUnreadIndicators();
|
this.updateUnreadIndicators();
|
||||||
@@ -179,13 +189,19 @@ export default class Sidebar extends React.Component {
|
|||||||
ChannelStore.removeChangeListener(this.onChange);
|
ChannelStore.removeChangeListener(this.onChange);
|
||||||
UserStore.removeChangeListener(this.onChange);
|
UserStore.removeChangeListener(this.onChange);
|
||||||
UserStore.removeInTeamChangeListener(this.onChange);
|
UserStore.removeInTeamChangeListener(this.onChange);
|
||||||
|
UserStore.removeInChannelChangeListener(this.onChange);
|
||||||
UserStore.removeStatusesChangeListener(this.onChange);
|
UserStore.removeStatusesChangeListener(this.onChange);
|
||||||
TeamStore.removeChangeListener(this.onChange);
|
TeamStore.removeChangeListener(this.onChange);
|
||||||
PreferenceStore.removeChangeListener(this.onChange);
|
PreferenceStore.removeChangeListener(this.onChange);
|
||||||
|
ModalStore.removeModalListener(ActionTypes.TOGGLE_DM_MODAL, this.onModalChange);
|
||||||
document.removeEventListener('keydown', this.navigateChannelShortcut);
|
document.removeEventListener('keydown', this.navigateChannelShortcut);
|
||||||
document.removeEventListener('keydown', this.navigateUnreadChannelShortcut);
|
document.removeEventListener('keydown', this.navigateUnreadChannelShortcut);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onModalChange(value, args) {
|
||||||
|
this.showMoreDirectChannelsModal(args.startingUsers);
|
||||||
|
}
|
||||||
|
|
||||||
onChange() {
|
onChange() {
|
||||||
if (this.state.currentTeam.id !== TeamStore.getCurrentId()) {
|
if (this.state.currentTeam.id !== TeamStore.getCurrentId()) {
|
||||||
ChannelStore.clear();
|
ChannelStore.clear();
|
||||||
@@ -203,11 +219,13 @@ export default class Sidebar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let currentChannelName = channel.display_name;
|
let currentChannelName = channel.display_name;
|
||||||
if (channel.type === 'D') {
|
if (channel.type === Constants.DM_CHANNEL) {
|
||||||
const teammate = Utils.getDirectTeammate(channel.id);
|
const teammate = Utils.getDirectTeammate(channel.id);
|
||||||
if (teammate != null) {
|
if (teammate != null) {
|
||||||
currentChannelName = teammate.username;
|
currentChannelName = teammate.username;
|
||||||
}
|
}
|
||||||
|
} else if (channel.type === Constants.GM_CHANNEL) {
|
||||||
|
currentChannelName = ChannelUtils.buildGroupChannelName(channel.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const unread = this.getTotalUnreadCount();
|
const unread = this.getTotalUnreadCount();
|
||||||
@@ -331,7 +349,7 @@ export default class Sidebar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDisplayedChannels() {
|
getDisplayedChannels() {
|
||||||
return this.state.favoriteChannels.concat(this.state.publicChannels).concat(this.state.privateChannels).concat(this.state.directChannels).concat(this.state.directNonTeamChannels);
|
return this.state.favoriteChannels.concat(this.state.publicChannels).concat(this.state.privateChannels).concat(this.state.directAndGroupChannels).concat(this.state.directNonTeamChannels);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLeaveDirectChannel(e, channel) {
|
handleLeaveDirectChannel(e, channel) {
|
||||||
@@ -340,9 +358,19 @@ export default class Sidebar extends React.Component {
|
|||||||
if (!this.isLeaving.get(channel.id)) {
|
if (!this.isLeaving.get(channel.id)) {
|
||||||
this.isLeaving.set(channel.id, true);
|
this.isLeaving.set(channel.id, true);
|
||||||
|
|
||||||
|
let id;
|
||||||
|
let category;
|
||||||
|
if (channel.type === Constants.DM_CHANNEL) {
|
||||||
|
id = channel.teammate_id;
|
||||||
|
category = Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW;
|
||||||
|
} else {
|
||||||
|
id = channel.id;
|
||||||
|
category = Constants.Preferences.CATEGORY_GROUP_CHANNEL_SHOW;
|
||||||
|
}
|
||||||
|
|
||||||
AsyncClient.savePreference(
|
AsyncClient.savePreference(
|
||||||
Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
|
category,
|
||||||
channel.teammate_id,
|
id,
|
||||||
'false',
|
'false',
|
||||||
() => {
|
() => {
|
||||||
this.isLeaving.set(channel.id, false);
|
this.isLeaving.set(channel.id, false);
|
||||||
@@ -382,13 +410,13 @@ export default class Sidebar extends React.Component {
|
|||||||
this.setState({newChannelModalType: ''});
|
this.setState({newChannelModalType: ''});
|
||||||
}
|
}
|
||||||
|
|
||||||
showMoreDirectChannelsModal() {
|
showMoreDirectChannelsModal(startingUsers) {
|
||||||
trackEvent('ui', 'ui_channels_more_direct');
|
trackEvent('ui', 'ui_channels_more_direct');
|
||||||
this.setState({showDirectChannelsModal: true});
|
this.setState({showDirectChannelsModal: true, startingUsers});
|
||||||
}
|
}
|
||||||
|
|
||||||
hideMoreDirectChannelsModal() {
|
hideMoreDirectChannelsModal() {
|
||||||
this.setState({showDirectChannelsModal: false});
|
this.setState({showDirectChannelsModal: false, startingUsers: null});
|
||||||
}
|
}
|
||||||
|
|
||||||
openLeftSidebar() {
|
openLeftSidebar() {
|
||||||
@@ -509,11 +537,16 @@ export default class Sidebar extends React.Component {
|
|||||||
rowClass += ' has-badge';
|
rowClass += ' has-badge';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let displayName = channel.display_name;
|
||||||
|
|
||||||
var icon = null;
|
var icon = null;
|
||||||
if (channel.type === Constants.OPEN_CHANNEL) {
|
if (channel.type === Constants.OPEN_CHANNEL) {
|
||||||
icon = <div className='status'><i className='fa fa-globe'/></div>;
|
icon = <div className='status'><i className='fa fa-globe'/></div>;
|
||||||
} else if (channel.type === Constants.PRIVATE_CHANNEL) {
|
} else if (channel.type === Constants.PRIVATE_CHANNEL) {
|
||||||
icon = <div className='status'><i className='fa fa-lock'/></div>;
|
icon = <div className='status'><i className='fa fa-lock'/></div>;
|
||||||
|
} else if (channel.type === Constants.GM_CHANNEL) {
|
||||||
|
displayName = ChannelUtils.buildGroupChannelName(channel.id);
|
||||||
|
icon = <div className='status status--group'>{UserStore.getProfileListInChannel(channel.id, true).length}</div>;
|
||||||
} else {
|
} else {
|
||||||
// set up status icon for direct message channels (status is null for other channel types)
|
// set up status icon for direct message channels (status is null for other channel types)
|
||||||
icon = (
|
icon = (
|
||||||
@@ -576,7 +609,7 @@ export default class Sidebar extends React.Component {
|
|||||||
onClick={this.trackChannelSelectedEvent}
|
onClick={this.trackChannelSelectedEvent}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
{channel.display_name}
|
{displayName}
|
||||||
{badge}
|
{badge}
|
||||||
{closeButton}
|
{closeButton}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -615,27 +648,10 @@ export default class Sidebar extends React.Component {
|
|||||||
|
|
||||||
const privateChannelItems = this.state.privateChannels.map(this.createChannelElement);
|
const privateChannelItems = this.state.privateChannels.map(this.createChannelElement);
|
||||||
|
|
||||||
const directMessageItems = this.state.directChannels.map((channel, index, arr) => {
|
const directMessageItems = this.state.directAndGroupChannels.map((channel, index, arr) => {
|
||||||
return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel);
|
return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel);
|
||||||
});
|
});
|
||||||
|
|
||||||
const directMessageNonTeamItems = this.state.directNonTeamChannels.map((channel, index, arr) => {
|
|
||||||
return this.createChannelElement(channel, index, arr, this.handleLeaveDirectChannel);
|
|
||||||
});
|
|
||||||
|
|
||||||
let directDivider;
|
|
||||||
if (directMessageNonTeamItems.length !== 0) {
|
|
||||||
directDivider =
|
|
||||||
(<div className='sidebar__divider'>
|
|
||||||
<div className='sidebar__divider__text'>
|
|
||||||
<FormattedMessage
|
|
||||||
id='sidebar.otherMembers'
|
|
||||||
defaultMessage='Outside this team'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the favicon to show if there are any notifications
|
// update the favicon to show if there are any notifications
|
||||||
if (this.lastBadgesActive !== this.badgesActive) {
|
if (this.lastBadgesActive !== this.badgesActive) {
|
||||||
var link = document.createElement('link');
|
var link = document.createElement('link');
|
||||||
@@ -659,7 +675,7 @@ export default class Sidebar extends React.Component {
|
|||||||
<li key='more'>
|
<li key='more'>
|
||||||
<a
|
<a
|
||||||
href='#'
|
href='#'
|
||||||
onClick={this.showMoreDirectChannelsModal}
|
onClick={() => this.showMoreDirectChannelsModal()}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='sidebar.moreElips'
|
id='sidebar.moreElips'
|
||||||
@@ -753,6 +769,7 @@ export default class Sidebar extends React.Component {
|
|||||||
moreDirectChannelsModal = (
|
moreDirectChannelsModal = (
|
||||||
<MoreDirectChannels
|
<MoreDirectChannels
|
||||||
onModalDismissed={this.hideMoreDirectChannelsModal}
|
onModalDismissed={this.hideMoreDirectChannelsModal}
|
||||||
|
startingUsers={this.state.startingUsers}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -866,8 +883,6 @@ export default class Sidebar extends React.Component {
|
|||||||
</h4>
|
</h4>
|
||||||
</li>
|
</li>
|
||||||
{directMessageItems}
|
{directMessageItems}
|
||||||
{directDivider}
|
|
||||||
{directMessageNonTeamItems}
|
|
||||||
{directMessageMore}
|
{directMessageMore}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default class StatusIcon extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className='status'
|
className={'status ' + this.props.className}
|
||||||
dangerouslySetInnerHTML={{__html: statusIcon}}
|
dangerouslySetInnerHTML={{__html: statusIcon}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -41,7 +41,12 @@ export default class StatusIcon extends React.Component {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StatusIcon.defaultProps = {
|
||||||
|
className: ''
|
||||||
|
};
|
||||||
|
|
||||||
StatusIcon.propTypes = {
|
StatusIcon.propTypes = {
|
||||||
status: React.PropTypes.string,
|
status: React.PropTypes.string,
|
||||||
|
className: React.PropTypes.string,
|
||||||
type: React.PropTypes.string
|
type: React.PropTypes.string
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import Client from 'client/web_client.jsx';
|
|||||||
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
|
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
|
||||||
import {Constants, ActionTypes} from 'utils/constants.jsx';
|
import {Constants, ActionTypes} from 'utils/constants.jsx';
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import {sortChannelsByDisplayName} from 'utils/channel_utils.jsx';
|
import {sortChannelsByDisplayName, buildGroupChannelName} from 'utils/channel_utils.jsx';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
@@ -25,12 +25,15 @@ class SwitchChannelSuggestion extends Suggestion {
|
|||||||
className += ' suggestion--selected';
|
className += ' suggestion--selected';
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayName = item.display_name;
|
let displayName = item.display_name;
|
||||||
let icon = null;
|
let icon = null;
|
||||||
if (item.type === Constants.OPEN_CHANNEL) {
|
if (item.type === Constants.OPEN_CHANNEL) {
|
||||||
icon = <div className='status'><i className='fa fa-globe'/></div>;
|
icon = <div className='status'><i className='fa fa-globe'/></div>;
|
||||||
} else if (item.type === Constants.PRIVATE_CHANNEL) {
|
} else if (item.type === Constants.PRIVATE_CHANNEL) {
|
||||||
icon = <div className='status'><i className='fa fa-lock'/></div>;
|
icon = <div className='status'><i className='fa fa-lock'/></div>;
|
||||||
|
} else if (item.type === Constants.GM_CHANNEL) {
|
||||||
|
displayName = buildGroupChannelName(item.id);
|
||||||
|
icon = <div className='status status--group'>{UserStore.getProfileListInChannel(item.id, true).length}</div>;
|
||||||
} else {
|
} else {
|
||||||
icon = (
|
icon = (
|
||||||
<div className='pull-left'>
|
<div className='pull-left'>
|
||||||
@@ -74,7 +77,11 @@ export default class SwitchChannelProvider extends Provider {
|
|||||||
for (const id of Object.keys(allChannels)) {
|
for (const id of Object.keys(allChannels)) {
|
||||||
const channel = allChannels[id];
|
const channel = allChannels[id];
|
||||||
if (channel.display_name.toLowerCase().indexOf(channelPrefix.toLowerCase()) !== -1) {
|
if (channel.display_name.toLowerCase().indexOf(channelPrefix.toLowerCase()) !== -1) {
|
||||||
channels.push(channel);
|
const newChannel = Object.assign({}, channel);
|
||||||
|
if (newChannel.type === Constants.GM_CHANNEL) {
|
||||||
|
newChannel.name = buildGroupChannelName(newChannel.id);
|
||||||
|
}
|
||||||
|
channels.push(newChannel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
{
|
{
|
||||||
|
"multiselect.go": "Go",
|
||||||
|
"multiselect.instructions": "Use up/down arrows to navigate and enter to select",
|
||||||
|
"multiselect.placeholder": "Search and add members",
|
||||||
|
"multiselect.numRemaining": "You can add {num, number} more",
|
||||||
|
"multiselect.numPeopleRemaining": "You can add {num, number} more {num, plural, =0 {people} one {person} other {people}}. ",
|
||||||
|
"filtered_channels_list.count": "{count} {count, plural, =0 {0 channels} one {channel} other {channels}}",
|
||||||
"about.close": "Close",
|
"about.close": "Close",
|
||||||
"about.copyright": "Copyright 2016 Mattermost, Inc. All rights reserved",
|
"about.copyright": "Copyright 2016 Mattermost, Inc. All rights reserved",
|
||||||
"about.database": "Database:",
|
"about.database": "Database:",
|
||||||
@@ -1638,6 +1644,8 @@
|
|||||||
"more_channels.prev": "Previous",
|
"more_channels.prev": "Previous",
|
||||||
"more_channels.title": "More Channels",
|
"more_channels.title": "More Channels",
|
||||||
"more_direct_channels.close": "Close",
|
"more_direct_channels.close": "Close",
|
||||||
|
"more_direct_channels.new_convo_note": "This will start a new conversation. If you’re adding a lot of people, consider creating a private group instead.",
|
||||||
|
"more_direct_channels.new_convo_note.full": "You’ve reached the maximum number of people for this conversation. Consider creating a private group instead.",
|
||||||
"more_direct_channels.message": "Message",
|
"more_direct_channels.message": "Message",
|
||||||
"more_direct_channels.title": "Direct Messages",
|
"more_direct_channels.title": "Direct Messages",
|
||||||
"msg_typing.areTyping": "{users} and {last} are typing...",
|
"msg_typing.areTyping": "{users} and {last} are typing...",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"react-dom": "15.4.2",
|
"react-dom": "15.4.2",
|
||||||
"react-intl": "2.2.3",
|
"react-intl": "2.2.3",
|
||||||
"react-router": "2.8.1",
|
"react-router": "2.8.1",
|
||||||
"react-select": "1.0.0-rc.3",
|
"react-select": "1.0.0-rc.2",
|
||||||
"superagent": "3.4.1",
|
"superagent": "3.4.1",
|
||||||
"twemoji": "2.2.3",
|
"twemoji": "2.2.3",
|
||||||
"velocity-animate": "1.4.2",
|
"velocity-animate": "1.4.2",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import BrowserStore from 'stores/browser_store.jsx';
|
|||||||
import emojiRoute from 'routes/route_emoji.jsx';
|
import emojiRoute from 'routes/route_emoji.jsx';
|
||||||
import integrationsRoute from 'routes/route_integrations.jsx';
|
import integrationsRoute from 'routes/route_integrations.jsx';
|
||||||
|
|
||||||
import {loadNewDMIfNeeded, loadProfilesAndTeamMembersForDMSidebar} from 'actions/user_actions.jsx';
|
import {loadNewDMIfNeeded, loadNewGMIfNeeded, loadProfilesForSidebar} from 'actions/user_actions.jsx';
|
||||||
|
|
||||||
function onChannelEnter(nextState, replace, callback) {
|
function onChannelEnter(nextState, replace, callback) {
|
||||||
doChannelChange(nextState, replace, callback);
|
doChannelChange(nextState, replace, callback);
|
||||||
@@ -36,6 +36,8 @@ function doChannelChange(state, replace, callback) {
|
|||||||
|
|
||||||
if (channel && channel.type === Constants.DM_CHANNEL) {
|
if (channel && channel.type === Constants.DM_CHANNEL) {
|
||||||
loadNewDMIfNeeded(Utils.getUserIdFromChannelName(channel));
|
loadNewDMIfNeeded(Utils.getUserIdFromChannelName(channel));
|
||||||
|
} else if (channel && channel.type === Constants.GM_CHANNEL) {
|
||||||
|
loadNewGMIfNeeded(channel.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
@@ -120,7 +122,7 @@ function preNeedsTeam(nextState, replace, callback) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
loadStatusesForChannelAndSidebar();
|
loadStatusesForChannelAndSidebar();
|
||||||
loadProfilesAndTeamMembersForDMSidebar();
|
loadProfilesForSidebar();
|
||||||
|
|
||||||
d1.resolve();
|
d1.resolve();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -170,6 +170,10 @@
|
|||||||
float: left;
|
float: left;
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
line-height: 27px;
|
line-height: 27px;
|
||||||
|
max-width: calc(100% - 80px);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
color: $white;
|
color: $white;
|
||||||
@@ -479,6 +483,10 @@
|
|||||||
.filter-row {
|
.filter-row {
|
||||||
margin: 5px 0 10px;
|
margin: 5px 0 10px;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
|
||||||
|
&.filter-row--full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-count {
|
.member-count {
|
||||||
@@ -582,6 +590,24 @@
|
|||||||
border-bottom: 1px solid $light-gray;
|
border-bottom: 1px solid $light-gray;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
|
|
||||||
|
&.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.more-modal__actions--round {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-modal__actions--round {
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
opacity: 0;
|
||||||
|
text-align: center;
|
||||||
|
width: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
@@ -611,6 +637,7 @@
|
|||||||
|
|
||||||
.more-modal__list {
|
.more-modal__list {
|
||||||
flex-grow: 500;
|
flex-grow: 500;
|
||||||
|
height: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-button {
|
.filter-button {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
@import 'links';
|
@import 'links';
|
||||||
@import 'mentions';
|
@import 'mentions';
|
||||||
@import 'modal';
|
@import 'modal';
|
||||||
|
@import 'multi-select';
|
||||||
@import 'oauth';
|
@import 'oauth';
|
||||||
@import 'popover';
|
@import 'popover';
|
||||||
@import 'save-button';
|
@import 'save-button';
|
||||||
|
|||||||
59
webapp/sass/components/_multi-select.scss
Обычный файл
59
webapp/sass/components/_multi-select.scss
Обычный файл
@@ -0,0 +1,59 @@
|
|||||||
|
@charset 'UTF-8';
|
||||||
|
|
||||||
|
.multi-select__container {
|
||||||
|
display: table;
|
||||||
|
padding: 0 15px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.Select {
|
||||||
|
display: table-cell;
|
||||||
|
padding-right: 15px;
|
||||||
|
vertical-align: top;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: table-cell;
|
||||||
|
height: 36px;
|
||||||
|
min-width: 60px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Select-control {
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Select-value {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Select-value-label {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-select__help {
|
||||||
|
padding: 10px 15px 0;
|
||||||
|
|
||||||
|
> div:not(.multi-select__note),
|
||||||
|
> span {
|
||||||
|
@include opacity(.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.multi-select__note {
|
||||||
|
@include border-radius(3px);
|
||||||
|
display: table;
|
||||||
|
margin-top: 5px;
|
||||||
|
padding: 8px 13px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: table-cell;
|
||||||
|
|
||||||
|
&.note__icon {
|
||||||
|
width: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,28 @@
|
|||||||
top: 1px;
|
top: 1px;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
|
|
||||||
|
&.status--group {
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
height: 16px;
|
||||||
|
left: 1px;
|
||||||
|
line-height: 15px;
|
||||||
|
margin-left: -3px;
|
||||||
|
text-align: center;
|
||||||
|
top: -1px;
|
||||||
|
width: 16px;
|
||||||
|
|
||||||
|
.mentions__name & {
|
||||||
|
height: 20px;
|
||||||
|
left: 0;
|
||||||
|
line-height: 20px;
|
||||||
|
margin-left: 0;
|
||||||
|
text-align: center;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
max-height: 14px;
|
max-height: 14px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
|
|
||||||
&.dropdown {
|
&.dropdown {
|
||||||
float: left;
|
float: left;
|
||||||
max-width: 100%;
|
max-width: 90%;
|
||||||
padding-right: 1em;
|
padding-right: 1em;
|
||||||
|
|
||||||
.header-dropdown__icon {
|
.header-dropdown__icon {
|
||||||
@@ -182,7 +182,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.channel-intro-profile {
|
.channel-intro-profile {
|
||||||
margin-left: 63px;
|
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
|
||||||
.user-popover {
|
.user-popover {
|
||||||
@@ -193,7 +192,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.channel-intro-img {
|
.channel-intro-img {
|
||||||
float: left;
|
.status-wrapper {
|
||||||
|
height: 50px;
|
||||||
|
margin: 0 10px 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@include border-radius(100px);
|
@include border-radius(100px);
|
||||||
@@ -211,10 +213,6 @@
|
|||||||
background: $bg--gray;
|
background: $bg--gray;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-intro-text {
|
|
||||||
margin-top: 35px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Team Header in Sidebar
|
// Team Header in Sidebar
|
||||||
|
|||||||
@@ -1,6 +1,23 @@
|
|||||||
@charset 'UTF-8';
|
@charset 'UTF-8';
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
|
.multi-select__container {
|
||||||
|
.btn {
|
||||||
|
display: block;
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Select-value-label {
|
||||||
|
max-width: 190px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-modal__list {
|
||||||
|
.more-modal__actions--round {
|
||||||
|
@include opacity(.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.post-create__container {
|
.post-create__container {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class ModalStoreClass extends EventEmitter {
|
|||||||
case ActionTypes.TOGGLE_GET_POST_LINK_MODAL:
|
case ActionTypes.TOGGLE_GET_POST_LINK_MODAL:
|
||||||
case ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL:
|
case ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL:
|
||||||
case ActionTypes.TOGGLE_GET_PUBLIC_LINK_MODAL:
|
case ActionTypes.TOGGLE_GET_PUBLIC_LINK_MODAL:
|
||||||
|
case ActionTypes.TOGGLE_DM_MODAL:
|
||||||
this.emit(type, value, args);
|
this.emit(type, value, args);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import UserStore from './user_store.jsx';
|
|||||||
import ChannelStore from './channel_store.jsx';
|
import ChannelStore from './channel_store.jsx';
|
||||||
import * as UserAgent from 'utils/user_agent.jsx';
|
import * as UserAgent from 'utils/user_agent.jsx';
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
|
import {buildGroupChannelName} from 'utils/channel_utils.jsx';
|
||||||
import * as PostUtils from 'utils/post_utils.jsx';
|
import * as PostUtils from 'utils/post_utils.jsx';
|
||||||
const ActionTypes = Constants.ActionTypes;
|
const ActionTypes = Constants.ActionTypes;
|
||||||
|
|
||||||
@@ -74,6 +75,8 @@ class NotificationStoreClass extends EventEmitter {
|
|||||||
};
|
};
|
||||||
} else if (channel.type === Constants.DM_CHANNEL) {
|
} else if (channel.type === Constants.DM_CHANNEL) {
|
||||||
title = Utils.localizeMessage('notification.dm', 'Direct Message');
|
title = Utils.localizeMessage('notification.dm', 'Direct Message');
|
||||||
|
} else if (channel.type === Constants.GM_CHANNEL) {
|
||||||
|
title = buildGroupChannelName(channel.id);
|
||||||
} else {
|
} else {
|
||||||
title = channel.display_name;
|
title = channel.display_name;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -414,13 +414,18 @@ class UserStoreClass extends EventEmitter {
|
|||||||
userIds.splice(index, 1);
|
userIds.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
getProfileListInChannel(channelId = ChannelStore.getCurrentId()) {
|
getProfileListInChannel(channelId = ChannelStore.getCurrentId(), skipCurrent = false) {
|
||||||
const userIds = this.profiles_in_channel[channelId] || [];
|
const userIds = this.profiles_in_channel[channelId] || [];
|
||||||
|
const currentId = this.getCurrentId();
|
||||||
const profiles = [];
|
const profiles = [];
|
||||||
|
|
||||||
for (let i = 0; i < userIds.length; i++) {
|
for (let i = 0; i < userIds.length; i++) {
|
||||||
const profile = this.getProfile(userIds[i]);
|
const profile = this.getProfile(userIds[i]);
|
||||||
if (profile) {
|
if (profile) {
|
||||||
|
if (skipCurrent && profile.id === currentId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
profiles.push(profile);
|
profiles.push(profile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,37 @@ describe('Client.Channels', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('createGroupChannel', function(done) {
|
||||||
|
TestHelper.initBasic(() => {
|
||||||
|
TestHelper.basicClient().createUser(
|
||||||
|
TestHelper.fakeUser(),
|
||||||
|
(user1) => {
|
||||||
|
TestHelper.basicClient().createUser(
|
||||||
|
TestHelper.fakeUser(),
|
||||||
|
function(user2) {
|
||||||
|
TestHelper.basicClient().createGroupChannel(
|
||||||
|
[user2.id, user1.id],
|
||||||
|
function(data) {
|
||||||
|
assert.equal(data.id.length > 0, true);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
function(err) {
|
||||||
|
done(new Error(err.message));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
function(err) {
|
||||||
|
done(new Error(err.message));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
function(err) {
|
||||||
|
done(new Error(err.message));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('updateChannel', function(done) {
|
it('updateChannel', function(done) {
|
||||||
TestHelper.initBasic(() => {
|
TestHelper.initBasic(() => {
|
||||||
var channel = TestHelper.basicChannel();
|
var channel = TestHelper.basicChannel();
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ export function createChannelIntroMessage(channel, fullWidthIntro) {
|
|||||||
centeredIntro = 'channel-intro--centered';
|
centeredIntro = 'channel-intro--centered';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channel.type === 'D') {
|
if (channel.type === Constants.DM_CHANNEL) {
|
||||||
return createDMIntroMessage(channel, centeredIntro);
|
return createDMIntroMessage(channel, centeredIntro);
|
||||||
|
} else if (channel.type === Constants.GM_CHANNEL) {
|
||||||
|
return createGMIntroMessage(channel, centeredIntro);
|
||||||
} else if (ChannelStore.isDefault(channel)) {
|
} else if (ChannelStore.isDefault(channel)) {
|
||||||
return createDefaultIntroMessage(channel, centeredIntro);
|
return createDefaultIntroMessage(channel, centeredIntro);
|
||||||
} else if (channel.name === Constants.OFFTOPIC_CHANNEL) {
|
} else if (channel.name === Constants.OFFTOPIC_CHANNEL) {
|
||||||
@@ -35,6 +37,65 @@ export function createChannelIntroMessage(channel, fullWidthIntro) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createGMIntroMessage(channel, centeredIntro) {
|
||||||
|
const profiles = UserStore.getProfileListInChannel(channel.id, true);
|
||||||
|
|
||||||
|
if (profiles.length > 0) {
|
||||||
|
const pictures = [];
|
||||||
|
let names = '';
|
||||||
|
for (let i = 0; i < profiles.length; i++) {
|
||||||
|
const profile = profiles[i];
|
||||||
|
|
||||||
|
pictures.push(
|
||||||
|
<ProfilePicture
|
||||||
|
key={'introprofilepicture' + profile.id}
|
||||||
|
src={Client.getUsersRoute() + '/' + profile.id + '/image?time=' + profile.last_picture_update}
|
||||||
|
width='50'
|
||||||
|
height='50'
|
||||||
|
user={profile}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (i === profiles.length - 1) {
|
||||||
|
names += Utils.displayUsernameForUser(profile);
|
||||||
|
} else if (i === profiles.length - 2) {
|
||||||
|
names += Utils.displayUsernameForUser(profile) + ' and ';
|
||||||
|
} else {
|
||||||
|
names += Utils.displayUsernameForUser(profile) + ', ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'channel-intro ' + centeredIntro}>
|
||||||
|
<div className='post-profile-img__container channel-intro-img'>
|
||||||
|
{pictures}
|
||||||
|
</div>
|
||||||
|
<p className='channel-intro-text'>
|
||||||
|
<FormattedHTMLMessage
|
||||||
|
id='intro_messages.GM'
|
||||||
|
defaultMessage='This is the start of your group message history with {names}.<br />Messages and files shared here are not shown to people outside this area.'
|
||||||
|
values={{
|
||||||
|
names
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
{createSetHeaderButton(channel)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'channel-intro ' + centeredIntro}>
|
||||||
|
<p className='channel-intro-text'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='intro_messages.group_message'
|
||||||
|
defaultMessage='This is the start of your group message history with these teammates. Messages and files shared here are not shown to people outside this area.'
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function createDMIntroMessage(channel, centeredIntro) {
|
export function createDMIntroMessage(channel, centeredIntro) {
|
||||||
var teammate = Utils.getDirectTeammate(channel.id);
|
var teammate = Utils.getDirectTeammate(channel.id);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ const Preferences = Constants.Preferences;
|
|||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
|
|
||||||
import UserStore from 'stores/user_store.jsx';
|
import UserStore from 'stores/user_store.jsx';
|
||||||
import TeamStore from 'stores/team_store.jsx';
|
|
||||||
import PreferenceStore from 'stores/preference_store.jsx';
|
import PreferenceStore from 'stores/preference_store.jsx';
|
||||||
import LocalizationStore from 'stores/localization_store.jsx';
|
import LocalizationStore from 'stores/localization_store.jsx';
|
||||||
|
|
||||||
@@ -15,30 +14,28 @@ import LocalizationStore from 'stores/localization_store.jsx';
|
|||||||
* Example: {
|
* Example: {
|
||||||
* publicChannels: [...],
|
* publicChannels: [...],
|
||||||
* privateChannels: [...],
|
* privateChannels: [...],
|
||||||
* directChannels: [...],
|
* directAndGroupChannels: [...],
|
||||||
* directNonTeamChannels: [...],
|
|
||||||
* favoriteChannels: [...]
|
* favoriteChannels: [...]
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export function buildDisplayableChannelList(persistentChannels) {
|
export function buildDisplayableChannelList(persistentChannels) {
|
||||||
const missingDMChannels = createMissingDirectChannels(persistentChannels);
|
const missingDirectChannels = createMissingDirectChannels(persistentChannels);
|
||||||
|
|
||||||
const channels = persistentChannels.
|
const channels = persistentChannels.
|
||||||
concat(missingDMChannels).
|
concat(missingDirectChannels).
|
||||||
map(completeDirectChannelInfo).
|
map(completeDirectChannelInfo).
|
||||||
filter(isNotDeletedChannel).
|
filter(isNotDeletedChannel).
|
||||||
sort(sortChannelsByDisplayName);
|
sort(sortChannelsByDisplayName);
|
||||||
|
|
||||||
const favoriteChannels = channels.filter(isFavoriteChannel);
|
const favoriteChannels = channels.filter(isFavoriteChannel);
|
||||||
const notFavoriteChannels = channels.filter(not(isFavoriteChannel));
|
const notFavoriteChannels = channels.filter(not(isFavoriteChannel));
|
||||||
const directChannels = notFavoriteChannels.filter(andX(isDirectChannel, isDirectChannelVisible));
|
const directAndGroupChannels = notFavoriteChannels.filter(orX(andX(isGroupChannel, isGroupChannelVisible), andX(isDirectChannel, isDirectChannelVisible)));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
favoriteChannels,
|
favoriteChannels,
|
||||||
publicChannels: notFavoriteChannels.filter(isOpenChannel),
|
publicChannels: notFavoriteChannels.filter(isOpenChannel),
|
||||||
privateChannels: notFavoriteChannels.filter(isPrivateChannel),
|
privateChannels: notFavoriteChannels.filter(isPrivateChannel),
|
||||||
directChannels: directChannels.filter(isConnectedToTeamMember),
|
directAndGroupChannels
|
||||||
directNonTeamChannels: directChannels.filter(isNotConnectedToTeamMember)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +59,14 @@ export function isPrivateChannel(channel) {
|
|||||||
return channel.type === Constants.PRIVATE_CHANNEL;
|
return channel.type === Constants.PRIVATE_CHANNEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isGroupChannel(channel) {
|
||||||
|
return channel.type === Constants.GM_CHANNEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGroupChannelVisible(channel) {
|
||||||
|
return PreferenceStore.getBool(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channel.id);
|
||||||
|
}
|
||||||
|
|
||||||
export function isDirectChannel(channel) {
|
export function isDirectChannel(channel) {
|
||||||
return channel.type === Constants.DM_CHANNEL;
|
return channel.type === Constants.DM_CHANNEL;
|
||||||
}
|
}
|
||||||
@@ -88,12 +93,12 @@ export function completeDirectChannelInfo(channel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultPrefix = 'D'; // fallback for future types
|
const defaultPrefix = 'D'; // fallback for future types
|
||||||
const typeToPrefixMap = {[Constants.OPEN_CHANNEL]: 'A', [Constants.PRIVATE_CHANNEL]: 'B', [Constants.DM_CHANNEL]: 'C'};
|
const typeToPrefixMap = {[Constants.OPEN_CHANNEL]: 'A', [Constants.PRIVATE_CHANNEL]: 'B', [Constants.DM_CHANNEL]: 'C', [Constants.GM_CHANNEL]: 'C'};
|
||||||
|
|
||||||
export function sortChannelsByDisplayName(a, b) {
|
export function sortChannelsByDisplayName(a, b) {
|
||||||
const locale = LocalizationStore.getLocale();
|
const locale = LocalizationStore.getLocale();
|
||||||
|
|
||||||
if (a.type !== b.type) {
|
if (a.type !== b.type && typeToPrefixMap[a.type] !== typeToPrefixMap[b.type]) {
|
||||||
return (typeToPrefixMap[a.type] || defaultPrefix).localeCompare((typeToPrefixMap[b.type] || defaultPrefix), locale);
|
return (typeToPrefixMap[a.type] || defaultPrefix).localeCompare((typeToPrefixMap[b.type] || defaultPrefix), locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +191,19 @@ export function showDeleteOption(channel, isAdmin, isSystemAdmin, isChannelAdmin
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildGroupChannelName(channelId) {
|
||||||
|
const profiles = UserStore.getProfileListInChannel(channelId, true);
|
||||||
|
let displayName = '';
|
||||||
|
for (let i = 0; i < profiles.length; i++) {
|
||||||
|
displayName += Utils.displayUsernameForUser(profiles[i]);
|
||||||
|
if (i !== profiles.length - 1) {
|
||||||
|
displayName += ', ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* not exported helpers
|
* not exported helpers
|
||||||
*/
|
*/
|
||||||
@@ -215,22 +233,14 @@ function createFakeChannelCurried(userId) {
|
|||||||
return (otherUserId) => createFakeChannel(userId, otherUserId);
|
return (otherUserId) => createFakeChannel(userId, otherUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isConnectedToTeamMember(channel) {
|
|
||||||
return isTeamMember(channel.teammate_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTeamMember(userId) {
|
|
||||||
return TeamStore.hasActiveMemberInTeam(TeamStore.getCurrentId(), userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isNotConnectedToTeamMember(channel) {
|
|
||||||
return TeamStore.hasMemberNotInTeam(TeamStore.getCurrentId(), channel.teammate_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function not(f) {
|
function not(f) {
|
||||||
return (...args) => !f(...args);
|
return (...args) => !f(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function orX(...fns) {
|
||||||
|
return (...args) => fns.some((f) => f(...args));
|
||||||
|
}
|
||||||
|
|
||||||
function andX(...fns) {
|
function andX(...fns) {
|
||||||
return (...args) => fns.every((f) => f(...args));
|
return (...args) => fns.every((f) => f(...args));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import windows10ThemeImage from 'images/themes/windows_dark.png';
|
|||||||
|
|
||||||
export const Preferences = {
|
export const Preferences = {
|
||||||
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
|
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
|
||||||
|
CATEGORY_GROUP_CHANNEL_SHOW: 'group_channel_show',
|
||||||
CATEGORY_DISPLAY_SETTINGS: 'display_settings',
|
CATEGORY_DISPLAY_SETTINGS: 'display_settings',
|
||||||
DISPLAY_PREFER_NICKNAME: 'nickname_full_name',
|
DISPLAY_PREFER_NICKNAME: 'nickname_full_name',
|
||||||
DISPLAY_PREFER_FULL_NAME: 'full_name',
|
DISPLAY_PREFER_FULL_NAME: 'full_name',
|
||||||
@@ -164,6 +165,7 @@ export const ActionTypes = keyMirror({
|
|||||||
TOGGLE_GET_POST_LINK_MODAL: null,
|
TOGGLE_GET_POST_LINK_MODAL: null,
|
||||||
TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null,
|
TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null,
|
||||||
TOGGLE_GET_PUBLIC_LINK_MODAL: null,
|
TOGGLE_GET_PUBLIC_LINK_MODAL: null,
|
||||||
|
TOGGLE_DM_MODAL: null,
|
||||||
|
|
||||||
SUGGESTION_PRETEXT_CHANGED: null,
|
SUGGESTION_PRETEXT_CHANGED: null,
|
||||||
SUGGESTION_RECEIVED_SUGGESTIONS: null,
|
SUGGESTION_RECEIVED_SUGGESTIONS: null,
|
||||||
@@ -390,8 +392,11 @@ export const Constants = {
|
|||||||
],
|
],
|
||||||
MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
||||||
MAX_DMS: 20,
|
MAX_DMS: 20,
|
||||||
|
MAX_USERS_IN_GM: 8,
|
||||||
|
MIN_USERS_IN_GM: 3,
|
||||||
MAX_CHANNEL_POPOVER_COUNT: 100,
|
MAX_CHANNEL_POPOVER_COUNT: 100,
|
||||||
DM_CHANNEL: 'D',
|
DM_CHANNEL: 'D',
|
||||||
|
GM_CHANNEL: 'G',
|
||||||
OPEN_CHANNEL: 'O',
|
OPEN_CHANNEL: 'O',
|
||||||
PRIVATE_CHANNEL: 'P',
|
PRIVATE_CHANNEL: 'P',
|
||||||
INVITE_TEAM: 'I',
|
INVITE_TEAM: 'I',
|
||||||
|
|||||||
@@ -477,6 +477,7 @@ export function applyTheme(theme) {
|
|||||||
changeCss('.sidebar--left .nav-pills__container li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.6));
|
changeCss('.sidebar--left .nav-pills__container li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.6));
|
||||||
changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText);
|
changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText);
|
||||||
changeCss('.sidebar--left .status .offline--icon', 'fill:' + theme.sidebarText);
|
changeCss('.sidebar--left .status .offline--icon', 'fill:' + theme.sidebarText);
|
||||||
|
changeCss('.sidebar--left .status.status--group', 'background:' + changeOpacity(theme.sidebarText, 0.3));
|
||||||
changeCss('@media(max-width: 768px){.app__body .modal .settings-modal .settings-table .nav>li>a, .app__body .sidebar--menu .divider', 'border-color:' + changeOpacity(theme.sidebarText, 0.2));
|
changeCss('@media(max-width: 768px){.app__body .modal .settings-modal .settings-table .nav>li>a, .app__body .sidebar--menu .divider', 'border-color:' + changeOpacity(theme.sidebarText, 0.2));
|
||||||
changeCss('@media(max-width: 768px){.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + changeOpacity(theme.sidebarText, 0.6));
|
changeCss('@media(max-width: 768px){.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + changeOpacity(theme.sidebarText, 0.6));
|
||||||
}
|
}
|
||||||
@@ -569,6 +570,7 @@ export function applyTheme(theme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (theme.centerChannelColor) {
|
if (theme.centerChannelColor) {
|
||||||
|
changeCss('.app__body .mentions__name .status.status--group, .app__body .multi-select__note', 'background:' + changeOpacity(theme.centerChannelColor, 0.12));
|
||||||
changeCss('.app__body .post-list__arrows, .app__body .post .flag-icon__container', 'fill:' + changeOpacity(theme.centerChannelColor, 0.3));
|
changeCss('.app__body .post-list__arrows, .app__body .post .flag-icon__container', 'fill:' + changeOpacity(theme.centerChannelColor, 0.3));
|
||||||
changeCss('.app__body .modal .status .offline--icon, .app__body .channel-header__links .icon, .app__body .sidebar--right .sidebar--right__subheader .usage__icon', 'fill:' + theme.centerChannelColor);
|
changeCss('.app__body .modal .status .offline--icon, .app__body .channel-header__links .icon, .app__body .sidebar--right .sidebar--right__subheader .usage__icon', 'fill:' + theme.centerChannelColor);
|
||||||
changeCss('@media(min-width: 768px){.app__body .post:hover .post__header .col__reply, .app__body .post.post--hovered .post__header .col__reply', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2));
|
changeCss('@media(min-width: 768px){.app__body .post:hover .post__header .col__reply, .app__body .post.post--hovered .post__header .col__reply', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2));
|
||||||
@@ -618,7 +620,7 @@ export function applyTheme(theme) {
|
|||||||
changeCss('@media(max-width: 1800px){.app__body .inner-wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07));
|
changeCss('@media(max-width: 1800px){.app__body .inner-wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07));
|
||||||
changeCss('.app__body .post.post--hovered', 'background:' + changeOpacity(theme.centerChannelColor, 0.08));
|
changeCss('.app__body .post.post--hovered', 'background:' + changeOpacity(theme.centerChannelColor, 0.08));
|
||||||
changeCss('@media(min-width: 768px){.app__body .post:hover, .app__body .more-modal__list .more-modal__row:hover, .app__body .modal .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.08));
|
changeCss('@media(min-width: 768px){.app__body .post:hover, .app__body .more-modal__list .more-modal__row:hover, .app__body .modal .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.08));
|
||||||
changeCss('.app__body .date-separator.hovered--before:after, .app__body .date-separator.hovered--after:before, .app__body .new-separator.hovered--after:before, .app__body .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07));
|
changeCss('.app__body .more-modal__row.more-modal__row--selected, .app__body .date-separator.hovered--before:after, .app__body .date-separator.hovered--after:before, .app__body .new-separator.hovered--after:before, .app__body .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07));
|
||||||
changeCss('@media(min-width: 768px){.app__body .suggestion-list__content .command:hover, .app__body .mentions__name:hover, .app__body .dropdown-menu>li>a:focus, .app__body .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15));
|
changeCss('@media(min-width: 768px){.app__body .suggestion-list__content .command:hover, .app__body .mentions__name:hover, .app__body .dropdown-menu>li>a:focus, .app__body .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15));
|
||||||
changeCss('.app__body .suggestion--selected, .app__body .bot-indicator', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
|
changeCss('.app__body .suggestion--selected, .app__body .bot-indicator', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
|
||||||
changeCss('code, .app__body .form-control[disabled], .app__body .form-control[readonly], .app__body fieldset[disabled] .form-control', 'background:' + changeOpacity(theme.centerChannelColor, 0.1));
|
changeCss('code, .app__body .form-control[disabled], .app__body .form-control[readonly], .app__body fieldset[disabled] .form-control', 'background:' + changeOpacity(theme.centerChannelColor, 0.1));
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user