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("/view", ApiUserRequired(viewChannel)).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_header", ApiUserRequired(updateChannelHeader)).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 {
|
||||
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)
|
||||
@@ -457,7 +490,7 @@ func getChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
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}, "")
|
||||
return
|
||||
}
|
||||
@@ -493,7 +526,7 @@ func getChannelByName(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
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}, "")
|
||||
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) {
|
||||
th := Setup().InitBasic().InitSystemAdmin()
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
if result := <-Srv.Store.Channel().Update(channel); result.Err != nil {
|
||||
return nil, result.Err
|
||||
@@ -702,7 +756,7 @@ func LeaveChannel(channelId string, userId string) *model.AppError {
|
||||
user := uresult.Data.(*model.User)
|
||||
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.StatusCode = http.StatusBadRequest
|
||||
return err
|
||||
|
||||
@@ -244,6 +244,8 @@ func renderBatchedPost(template *utils.HTMLTemplate, post *model.Post, teamName
|
||||
return ""
|
||||
} else if channel := result.Data.(*model.Channel); channel.Type == model.CHANNEL_DIRECT {
|
||||
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 {
|
||||
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)
|
||||
channelName := make(map[string]string)
|
||||
for _, id := range mentionedUsersList {
|
||||
senderName[id] = ""
|
||||
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
|
||||
@@ -259,7 +273,7 @@ func SendNotifications(post *model.Post, team *model.Team, channel *model.Channe
|
||||
}
|
||||
|
||||
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) {
|
||||
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 {
|
||||
if channel.Type == model.CHANNEL_DIRECT && 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
|
||||
if channel.IsGroupOrDirect() && channel.TeamId != team.Id {
|
||||
// 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 {
|
||||
return result.Err
|
||||
} else {
|
||||
@@ -381,6 +395,14 @@ func sendNotificationEmail(post *model.Post, user *model.User, channel *model.Ch
|
||||
mailTemplate = "api.templates.post_subject_in_direct_message"
|
||||
mailParameters = map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName,
|
||||
"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 {
|
||||
bodyText = userLocale("api.post.send_notifications_and_forget.mention_body")
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var channelName string
|
||||
|
||||
if channel.Type == model.CHANNEL_DIRECT {
|
||||
channelName = senderName
|
||||
} else {
|
||||
channelName = channel.DisplayName
|
||||
}
|
||||
|
||||
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 {
|
||||
msg.Category = model.CATEGORY_DM
|
||||
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
|
||||
} else {
|
||||
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 {
|
||||
if channel.Type != model.CHANNEL_DIRECT {
|
||||
if !channel.IsGroupOrDirect() {
|
||||
InvalidateCacheForChannelMembers(channel.Id)
|
||||
if result := <-Srv.Store.Channel().RemoveMember(channel.Id, user.Id); result.Err != nil {
|
||||
return result.Err
|
||||
|
||||
28
i18n/en.json
28
i18n/en.json
@@ -199,6 +199,14 @@
|
||||
"id": "api.channel.create_channel.direct_channel.app_error",
|
||||
"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",
|
||||
"translation": "Invalid character '__' in channel name for non-direct channel"
|
||||
@@ -887,6 +895,10 @@
|
||||
"id": "api.email_batching.render_batched_post.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",
|
||||
"translation": "Go to Post"
|
||||
@@ -2151,6 +2163,14 @@
|
||||
"id": "api.templates.post_subject_in_direct_message",
|
||||
"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",
|
||||
"translation": "Reset Password"
|
||||
@@ -3059,6 +3079,14 @@
|
||||
"id": "app.import.import_line.null_post.error",
|
||||
"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",
|
||||
"translation": "Ability to create new teams"
|
||||
|
||||
@@ -30,6 +30,7 @@ var PERMISSION_MANAGE_ROLES *Permission
|
||||
var PERMISSION_MANAGE_TEAM_ROLES *Permission
|
||||
var PERMISSION_MANAGE_CHANNEL_ROLES *Permission
|
||||
var PERMISSION_CREATE_DIRECT_CHANNEL *Permission
|
||||
var PERMISSION_CREATE_GROUP_CHANNEL *Permission
|
||||
var PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES *Permission
|
||||
var PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES *Permission
|
||||
var PERMISSION_LIST_TEAM_CHANNELS *Permission
|
||||
@@ -149,6 +150,11 @@ func InitalizePermissions() {
|
||||
"authentication.permissions.create_direct_channel.name",
|
||||
"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{
|
||||
"manage__publicchannel_properties",
|
||||
"authentication.permissions.manage_public_channel_properties.name",
|
||||
@@ -350,6 +356,7 @@ func InitalizeRoles() {
|
||||
"authentication.roles.global_user.description",
|
||||
[]string{
|
||||
PERMISSION_CREATE_DIRECT_CHANNEL.Id,
|
||||
PERMISSION_CREATE_GROUP_CHANNEL.Id,
|
||||
PERMISSION_PERMANENT_DELETE_USER.Id,
|
||||
PERMISSION_MANAGE_OAUTH.Id,
|
||||
},
|
||||
|
||||
@@ -4,8 +4,12 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
@@ -13,6 +17,9 @@ const (
|
||||
CHANNEL_OPEN = "O"
|
||||
CHANNEL_PRIVATE = "P"
|
||||
CHANNEL_DIRECT = "D"
|
||||
CHANNEL_GROUP = "G"
|
||||
CHANNEL_GROUP_MAX_USERS = 8
|
||||
CHANNEL_GROUP_MIN_USERS = 3
|
||||
DEFAULT_CHANNEL = "town-square"
|
||||
CHANNEL_DISPLAY_NAME_MAX_RUNES = 64
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -126,6 +133,10 @@ func (o *Channel) ExtraUpdated() {
|
||||
o.ExtraUpdateAt = GetMillis()
|
||||
}
|
||||
|
||||
func (o *Channel) IsGroupOrDirect() bool {
|
||||
return o.Type == CHANNEL_DIRECT || o.Type == CHANNEL_GROUP
|
||||
}
|
||||
|
||||
func GetDMNameFromIds(userId1, userId2 string) string {
|
||||
if userId1 > userId2 {
|
||||
return userId2 + "__" + userId1
|
||||
@@ -133,3 +144,31 @@ func GetDMNameFromIds(userId1, userId2 string) string {
|
||||
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.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) {
|
||||
if r, err := c.DoApiPost(c.GetTeamRoute()+"/channels/update", channel.ToJson()); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -16,6 +16,7 @@ const (
|
||||
WEBSOCKET_EVENT_CHANNEL_DELETED = "channel_deleted"
|
||||
WEBSOCKET_EVENT_CHANNEL_CREATED = "channel_created"
|
||||
WEBSOCKET_EVENT_DIRECT_ADDED = "direct_added"
|
||||
WEBSOCKET_EVENT_GROUP_ADDED = "group_added"
|
||||
WEBSOCKET_EVENT_NEW_USER = "new_user"
|
||||
WEBSOCKET_EVENT_LEAVE_TEAM = "leave_team"
|
||||
WEBSOCKET_EVENT_UPDATE_TEAM = "update_team"
|
||||
|
||||
@@ -211,7 +211,7 @@ func (s SqlChannelStore) saveChannelT(transaction *gorp.Transaction, channel *mo
|
||||
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 {
|
||||
result.Err = model.NewLocAppError("SqlChannelStore.Save", "store.sql_channel.save_channel.current_count.app_error", nil, "teamId="+channel.TeamId+", "+err.Error())
|
||||
return result
|
||||
|
||||
@@ -9,7 +9,7 @@ import ChannelStore from 'stores/channel_store.jsx';
|
||||
import * as ChannelUtils from 'utils/channel_utils.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 Client from 'client/web_client.jsx';
|
||||
@@ -22,8 +22,12 @@ import {browserHistory} from 'react-router/es6';
|
||||
|
||||
export function goToChannel(channel) {
|
||||
if (channel.fake) {
|
||||
const user = UserStore.getProfileByUsername(channel.display_name);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
openDirectChannelToUser(
|
||||
UserStore.getProfileByUsername(channel.display_name),
|
||||
user.id,
|
||||
() => {
|
||||
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name);
|
||||
},
|
||||
@@ -167,18 +171,18 @@ export function makeUserChannelMember(channelId, userId, success, error) {
|
||||
);
|
||||
}
|
||||
|
||||
export function openDirectChannelToUser(user, success, error) {
|
||||
const channelName = Utils.getDirectChannelName(UserStore.getCurrentId(), user.id);
|
||||
export function openDirectChannelToUser(userId, success, error) {
|
||||
const channelName = Utils.getDirectChannelName(UserStore.getCurrentId(), userId);
|
||||
const channel = ChannelStore.getByName(channelName);
|
||||
|
||||
if (channel) {
|
||||
trackEvent('api', 'api_channels_join_direct');
|
||||
PreferenceStore.setPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, user.id, 'true');
|
||||
loadProfilesAndTeamMembersForDMSidebar();
|
||||
PreferenceStore.setPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, userId, 'true');
|
||||
loadProfilesForSidebar();
|
||||
|
||||
AsyncClient.savePreference(
|
||||
Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
|
||||
user.id,
|
||||
userId,
|
||||
'true'
|
||||
);
|
||||
|
||||
@@ -190,7 +194,7 @@ export function openDirectChannelToUser(user, success, error) {
|
||||
}
|
||||
|
||||
Client.createDirectChannel(
|
||||
user.id,
|
||||
userId,
|
||||
(data) => {
|
||||
Client.getChannel(
|
||||
data.id,
|
||||
@@ -201,12 +205,12 @@ export function openDirectChannelToUser(user, success, error) {
|
||||
member: data2.member
|
||||
});
|
||||
|
||||
PreferenceStore.setPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, user.id, 'true');
|
||||
loadProfilesAndTeamMembersForDMSidebar();
|
||||
PreferenceStore.setPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, userId, 'true');
|
||||
loadProfilesForSidebar();
|
||||
|
||||
AsyncClient.savePreference(
|
||||
Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
|
||||
user.id,
|
||||
userId,
|
||||
'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) {
|
||||
trackEvent('api', 'api_channels_favorited');
|
||||
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 {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 {stopPeriodicStatusUpdates} from 'actions/status_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) {
|
||||
loadProfilesAndTeamMembersForDMSidebar();
|
||||
loadProfilesForSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import PostStore from 'stores/post_store.jsx';
|
||||
import UserStore from 'stores/user_store.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 Client from 'client/web_client.jsx';
|
||||
@@ -24,8 +24,12 @@ export function handleNewPost(post, msg) {
|
||||
websocketMessageProps = msg.data;
|
||||
}
|
||||
|
||||
if (msg && msg.data && msg.data.channel_type === Constants.DM_CHANNEL) {
|
||||
loadNewDMIfNeeded(post.user_id);
|
||||
if (msg && msg.data) {
|
||||
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) {
|
||||
|
||||
@@ -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) {
|
||||
if (userId === UserStore.getCurrentId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pref = PreferenceStore.get(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, userId, 'false');
|
||||
if (pref === 'false') {
|
||||
const pref = PreferenceStore.getBool(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, userId, false);
|
||||
if (pref === false) {
|
||||
PreferenceStore.setPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, userId, 'true');
|
||||
AsyncClient.savePreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, userId, 'true');
|
||||
loadProfilesAndTeamMembersForDMSidebar();
|
||||
loadProfilesForDM();
|
||||
}
|
||||
}
|
||||
|
||||
export function loadProfilesAndTeamMembersForDMSidebar() {
|
||||
const dmPrefs = PreferenceStore.getCategory(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
|
||||
const teamId = TeamStore.getCurrentId();
|
||||
const profilesToLoad = [];
|
||||
const membersToLoad = [];
|
||||
export function loadNewGMIfNeeded(channelId, userId) {
|
||||
if (userId === UserStore.getCurrentId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, value] of dmPrefs) {
|
||||
if (value === 'true') {
|
||||
if (!UserStore.hasProfile(key)) {
|
||||
profilesToLoad.push(key);
|
||||
}
|
||||
membersToLoad.push(key);
|
||||
function checkPreference() {
|
||||
const pref = PreferenceStore.getBool(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channelId, false);
|
||||
if (pref === false) {
|
||||
PreferenceStore.setPreference(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channelId, 'true');
|
||||
AsyncClient.savePreference(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channelId, 'true');
|
||||
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 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++) {
|
||||
const channel = channels[i];
|
||||
if (channel.type !== Constants.DM_CHANNEL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const member = channelMembers[channel.id];
|
||||
if (!member) {
|
||||
continue;
|
||||
}
|
||||
const teammateId = channel.name.replace(UserStore.getCurrentId(), '').replace('__', '');
|
||||
const isVisible = PreferenceStore.getBool(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammateId);
|
||||
|
||||
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({
|
||||
user_id: UserStore.getCurrentId(),
|
||||
category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
|
||||
name: teammateId,
|
||||
value: 'true'
|
||||
});
|
||||
|
||||
if (!UserStore.hasProfile(teammateId)) {
|
||||
profilesToLoad.push(teammateId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!UserStore.hasProfile(teammateId)) {
|
||||
profilesToLoad.push(teammateId);
|
||||
}
|
||||
profileIds.push(teammateId);
|
||||
}
|
||||
|
||||
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
|
||||
populateDMChannelsWithProfiles(membersToLoad);
|
||||
populateDMChannelsWithProfiles(profileIds);
|
||||
},
|
||||
(err) => {
|
||||
AsyncClient.dispatchError(err, 'getProfilesByIds');
|
||||
}
|
||||
);
|
||||
} else {
|
||||
populateDMChannelsWithProfiles(membersToLoad);
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
);
|
||||
populateDMChannelsWithProfiles(profileIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {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 * as StatusActions from 'actions/status_actions.jsx';
|
||||
|
||||
@@ -244,7 +244,7 @@ function handleUpdateTeamEvent(msg) {
|
||||
function handleDirectAddedEvent(msg) {
|
||||
AsyncClient.getChannel(msg.broadcast.channel_id);
|
||||
PreferenceStore.setPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, msg.data.teammate_id, 'true');
|
||||
loadProfilesAndTeamMembersForDMSidebar();
|
||||
loadProfilesForSidebar();
|
||||
}
|
||||
|
||||
function handleUserAddedEvent(msg) {
|
||||
|
||||
@@ -1278,6 +1278,16 @@ export default class Client {
|
||||
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) {
|
||||
request.
|
||||
post(`${this.getChannelsRoute()}/update`).
|
||||
|
||||
@@ -76,4 +76,4 @@ MultiSelectSetting.propTypes = {
|
||||
noResultText: React.PropTypes.node,
|
||||
errorText: React.PropTypes.node,
|
||||
notPresent: React.PropTypes.node
|
||||
};
|
||||
};
|
||||
|
||||
@@ -32,7 +32,9 @@ import {getSiteURL} from 'utils/url.jsx';
|
||||
import * as TextFormatting from 'utils/text_formatting.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 {FormattedMessage} from 'react-intl';
|
||||
@@ -53,6 +55,7 @@ export default class ChannelHeader extends React.Component {
|
||||
this.getFlagged = this.getFlagged.bind(this);
|
||||
this.initWebrtc = this.initWebrtc.bind(this);
|
||||
this.onBusy = this.onBusy.bind(this);
|
||||
this.openDirectMessageModal = this.openDirectMessageModal.bind(this);
|
||||
|
||||
const state = this.getStateFromStores();
|
||||
state.showEditChannelPurposeModal = false;
|
||||
@@ -198,6 +201,14 @@ export default class ChannelHeader extends React.Component {
|
||||
this.setState({isBusy});
|
||||
}
|
||||
|
||||
openDirectMessageModal() {
|
||||
AppDispatcher.handleViewAction({
|
||||
type: ActionTypes.TOGGLE_DM_MODAL,
|
||||
value: true,
|
||||
startingUsers: UserStore.getProfileListInChannel(this.props.channelId, true)
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const flagIcon = Constants.FLAG_ICON_SVG;
|
||||
|
||||
@@ -246,7 +257,8 @@ export default class ChannelHeader extends React.Component {
|
||||
const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
|
||||
const isSystemAdmin = UserStore.isSystemAdminForCurrentUser();
|
||||
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;
|
||||
|
||||
if (isDirect) {
|
||||
@@ -319,6 +331,10 @@ export default class ChannelHeader extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
if (isGroup) {
|
||||
channelTitle = ChannelUtils.buildGroupChannelName(channel.id);
|
||||
}
|
||||
|
||||
let channelTerm = (
|
||||
<FormattedMessage
|
||||
id='channel_header.channel'
|
||||
@@ -364,6 +380,64 @@ export default class ChannelHeader extends React.Component {
|
||||
</ToggleModalButton>
|
||||
</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 {
|
||||
dropdownContents.push(
|
||||
<li
|
||||
|
||||
@@ -105,7 +105,7 @@ export default class SwitchChannelModal extends React.Component {
|
||||
|
||||
if (user) {
|
||||
openDirectChannelToUser(
|
||||
user,
|
||||
user.id,
|
||||
(ch) => {
|
||||
channel = ch;
|
||||
this.switchToChannel(channel);
|
||||
@@ -117,7 +117,7 @@ export default class SwitchChannelModal extends React.Component {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
channel = ChannelStore.getByName(this.selected.name);
|
||||
channel = ChannelStore.get(this.selected.id);
|
||||
this.switchToChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import SearchableUserList from 'components/searchable_user_list.jsx';
|
||||
import SpinnerButton from 'components/spinner_button.jsx';
|
||||
import MultiSelect from 'components/multiselect/multiselect.jsx';
|
||||
import ProfilePicture from 'components/profile_picture.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 TeamStore from 'stores/team_store.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 {displayUsernameForUser} from 'utils/utils.jsx';
|
||||
import Client from 'client/web_client.jsx';
|
||||
|
||||
import React from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
@@ -21,6 +21,7 @@ import {FormattedMessage} from 'react-intl';
|
||||
import {browserHistory} from 'react-router/es6';
|
||||
|
||||
const USERS_PER_PAGE = 50;
|
||||
const MAX_SELECTABLE_VALUES = Constants.MAX_USERS_IN_GM - 1;
|
||||
|
||||
export default class MoreDirectChannels extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -28,21 +29,31 @@ export default class MoreDirectChannels extends React.Component {
|
||||
|
||||
this.handleHide = this.handleHide.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.createJoinDirectChannelButton = this.createJoinDirectChannelButton.bind(this);
|
||||
this.toggleList = this.toggleList.bind(this);
|
||||
this.nextPage = this.nextPage.bind(this);
|
||||
this.search = this.search.bind(this);
|
||||
this.addValue = this.addValue.bind(this);
|
||||
|
||||
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 = {
|
||||
users: null,
|
||||
loadingDMChannel: -1,
|
||||
listType: 'team',
|
||||
values,
|
||||
show: true,
|
||||
search: false
|
||||
search: false,
|
||||
loadingChannel: -1
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,17 +61,18 @@ export default class MoreDirectChannels extends React.Component {
|
||||
UserStore.addChangeListener(this.onChange);
|
||||
UserStore.addInTeamChangeListener(this.onChange);
|
||||
UserStore.addStatusesChangeListener(this.onChange);
|
||||
TeamStore.addChangeListener(this.onChange);
|
||||
|
||||
AsyncClient.getProfiles(0, Constants.PROFILE_CHUNK_SIZE);
|
||||
AsyncClient.getProfilesInTeam(TeamStore.getCurrentId(), 0, Constants.PROFILE_CHUNK_SIZE);
|
||||
if (this.listType === 'any') {
|
||||
AsyncClient.getProfiles(0, USERS_PER_PAGE * 2);
|
||||
} else {
|
||||
AsyncClient.getProfilesInTeam(TeamStore.getCurrentId(), 0, USERS_PER_PAGE * 2);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
UserStore.removeChangeListener(this.onChange);
|
||||
UserStore.removeInTeamChangeListener(this.onChange);
|
||||
UserStore.removeStatusesChangeListener(this.onChange);
|
||||
TeamStore.removeChangeListener(this.onChange);
|
||||
}
|
||||
|
||||
handleHide() {
|
||||
@@ -68,8 +80,8 @@ export default class MoreDirectChannels extends React.Component {
|
||||
}
|
||||
|
||||
handleExit() {
|
||||
if (this.exitToDirectChannel) {
|
||||
browserHistory.push(this.exitToDirectChannel);
|
||||
if (this.exitToChannel) {
|
||||
browserHistory.push(this.exitToChannel);
|
||||
}
|
||||
|
||||
if (this.props.onModalDismissed) {
|
||||
@@ -77,28 +89,49 @@ export default class MoreDirectChannels extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleShowDirectChannel(teammate, e) {
|
||||
e.preventDefault();
|
||||
handleSubmit(e) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (this.state.loadingDMChannel !== -1) {
|
||||
if (this.state.loadingChannel !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({loadingDMChannel: teammate.id});
|
||||
openDirectChannelToUser(
|
||||
teammate,
|
||||
(channel) => {
|
||||
// Due to how react-overlays Modal handles focus, we delay pushing
|
||||
// the new channel information until the modal is fully exited.
|
||||
// The channel information will be pushed in `handleExit`
|
||||
this.exitToDirectChannel = TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name;
|
||||
this.setState({loadingDMChannel: -1});
|
||||
this.handleHide();
|
||||
},
|
||||
() => {
|
||||
this.setState({loadingDMChannel: -1});
|
||||
}
|
||||
);
|
||||
const userIds = this.state.values.map((v) => v.id);
|
||||
if (userIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({loadingChannel: 1});
|
||||
|
||||
const success = (channel) => {
|
||||
// Due to how react-overlays Modal handles focus, we delay pushing
|
||||
// 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({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) {
|
||||
@@ -107,10 +140,17 @@ export default class MoreDirectChannels extends React.Component {
|
||||
}
|
||||
|
||||
let users;
|
||||
if (this.state.listType === 'any') {
|
||||
users = UserStore.getProfileList(true);
|
||||
if (this.listType === 'any') {
|
||||
users = Object.assign([], UserStore.getProfileList(true));
|
||||
} 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({
|
||||
@@ -118,72 +158,51 @@ export default class MoreDirectChannels extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
toggleList(e) {
|
||||
const listType = e.target.value;
|
||||
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') {
|
||||
handlePageChange(page, prevPage) {
|
||||
if (page > prevPage) {
|
||||
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) {
|
||||
clearTimeout(this.searchTimeoutId);
|
||||
|
||||
if (term === '') {
|
||||
this.onChange(true);
|
||||
this.setState({search: false});
|
||||
this.searchTimeoutId = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let teamId;
|
||||
if (this.state.listType === 'any') {
|
||||
if (this.listType === 'any') {
|
||||
teamId = '';
|
||||
} else {
|
||||
teamId = TeamStore.getCurrentId();
|
||||
}
|
||||
|
||||
clearTimeout(this.searchTimeoutId);
|
||||
|
||||
this.searchTimeoutId = setTimeout(
|
||||
const searchTimeoutId = setTimeout(
|
||||
() => {
|
||||
searchUsers(
|
||||
term,
|
||||
teamId,
|
||||
{},
|
||||
(users) => {
|
||||
if (searchTimeoutId !== this.searchTimeoutId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let indexToDelete = -1;
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
if (users[i].id === UserStore.getCurrentId()) {
|
||||
users.splice(i, 1);
|
||||
break;
|
||||
indexToDelete = i;
|
||||
}
|
||||
users[i].value = users[i].id;
|
||||
users[i].label = '@' + users[i].username;
|
||||
}
|
||||
|
||||
if (indexToDelete !== -1) {
|
||||
users.splice(indexToDelete, 1);
|
||||
}
|
||||
this.setState({search: true, users});
|
||||
}
|
||||
@@ -191,44 +210,88 @@ export default class MoreDirectChannels extends React.Component {
|
||||
},
|
||||
Constants.SEARCH_TIMEOUT_MILLISECONDS
|
||||
);
|
||||
|
||||
this.searchTimeoutId = searchTimeoutId;
|
||||
}
|
||||
|
||||
render() {
|
||||
let teamToggle;
|
||||
let memberClass = '';
|
||||
if (global.window.mm_config.RestrictDirectMessage === 'any') {
|
||||
memberClass = 'more-system-members';
|
||||
teamToggle = (
|
||||
<div className='member-select__container'>
|
||||
<select
|
||||
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>
|
||||
);
|
||||
handleDelete(values) {
|
||||
this.setState({values});
|
||||
}
|
||||
|
||||
renderOption(option, isSelected, onAdd) {
|
||||
var rowSelected = '';
|
||||
if (isSelected) {
|
||||
rowSelected = 'more-modal__row--selected';
|
||||
}
|
||||
|
||||
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 (
|
||||
<Modal
|
||||
dialogClassName={'more-modal more-direct-channels ' + memberClass}
|
||||
dialogClassName={'more-modal more-direct-channels'}
|
||||
show={this.state.show}
|
||||
onHide={this.handleHide}
|
||||
onExited={this.handleExit}
|
||||
@@ -242,15 +305,21 @@ export default class MoreDirectChannels extends React.Component {
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{teamToggle}
|
||||
<SearchableUserList
|
||||
key={'moreDirectChannelsList_' + this.state.listType}
|
||||
users={this.state.users}
|
||||
usersPerPage={USERS_PER_PAGE}
|
||||
nextPage={this.nextPage}
|
||||
search={this.search}
|
||||
actions={[this.createJoinDirectChannelButton]}
|
||||
focusOnMount={!UserAgent.isMobile()}
|
||||
<MultiSelect
|
||||
key='moreDirectChannelsList'
|
||||
options={this.state.users}
|
||||
optionRenderer={this.renderOption}
|
||||
values={this.state.values}
|
||||
valueRenderer={this.renderValue}
|
||||
perPage={USERS_PER_PAGE}
|
||||
handlePageChange={this.handlePageChange}
|
||||
handleInput={this.search}
|
||||
handleDelete={this.handleDelete}
|
||||
handleAdd={this.addValue}
|
||||
handleSubmit={this.handleSubmit}
|
||||
noteText={note}
|
||||
maxValues={MAX_SELECTABLE_VALUES}
|
||||
numRemainingText={numRemainingText}
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
@@ -259,5 +328,6 @@ export default class MoreDirectChannels extends React.Component {
|
||||
}
|
||||
|
||||
MoreDirectChannels.propTypes = {
|
||||
startingUsers: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
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.hideChannelSwitchModal = this.hideChannelSwitchModal.bind(this);
|
||||
|
||||
this.openDirectMessageModal = this.openDirectMessageModal.bind(this);
|
||||
|
||||
const state = this.getStateFromStores();
|
||||
state.showEditChannelPurposeModal = 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) => {
|
||||
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) {
|
||||
let channelTerm = (
|
||||
<FormattedMessage
|
||||
@@ -258,6 +268,57 @@ export default class Navbar extends React.Component {
|
||||
</a>
|
||||
</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 {
|
||||
viewInfoOption = (
|
||||
<li role='presentation'>
|
||||
@@ -621,6 +682,7 @@ export default class Navbar extends React.Component {
|
||||
var isSystemAdmin = false;
|
||||
var isChannelAdmin = false;
|
||||
var isDirect = false;
|
||||
let isGroup = false;
|
||||
|
||||
var editChannelHeaderModal = null;
|
||||
var editChannelPurposeModal = null;
|
||||
@@ -660,6 +722,9 @@ export default class Navbar extends React.Component {
|
||||
isDirect = true;
|
||||
const teammateId = Utils.getUserIdFromChannelName(channel);
|
||||
channelTitle = Utils.displayUsername(teammateId);
|
||||
} else if (channel.type === Constants.GM_CHANNEL) {
|
||||
isGroup = true;
|
||||
channelTitle = ChannelUtils.buildGroupChannelName(channel.id);
|
||||
}
|
||||
|
||||
if (channel.header.length === 0) {
|
||||
@@ -757,7 +822,7 @@ export default class Navbar extends React.Component {
|
||||
</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 (
|
||||
<div>
|
||||
|
||||
@@ -48,7 +48,7 @@ export default class PopoverListMembers extends React.Component {
|
||||
e.preventDefault();
|
||||
|
||||
openDirectChannelToUser(
|
||||
teammate,
|
||||
teammate.id,
|
||||
(channel, channelAlreadyExisted) => {
|
||||
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + channel.name);
|
||||
if (channelAlreadyExisted) {
|
||||
|
||||
@@ -81,7 +81,7 @@ export default class ProfilePopover extends React.Component {
|
||||
this.setState({loadingDMChannel: user.id});
|
||||
|
||||
openDirectChannelToUser(
|
||||
user,
|
||||
user.id,
|
||||
(channel) => {
|
||||
if (Utils.isMobile()) {
|
||||
GlobalActions.emitCloseRightHandSide();
|
||||
|
||||
@@ -15,6 +15,7 @@ import ChannelStore from 'stores/channel_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import TeamStore from 'stores/team_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 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 {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';
|
||||
|
||||
@@ -49,6 +50,8 @@ export default class Sidebar extends React.Component {
|
||||
this.getStateFromStores = this.getStateFromStores.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.updateUnreadIndicators = this.updateUnreadIndicators.bind(this);
|
||||
this.handleLeaveDirectChannel = this.handleLeaveDirectChannel.bind(this);
|
||||
@@ -77,6 +80,7 @@ export default class Sidebar extends React.Component {
|
||||
state.showDirectChannelsModal = false;
|
||||
state.showMoreChannelsModal = false;
|
||||
state.loadingDMChannel = -1;
|
||||
state.inChannelChange = false;
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@@ -96,7 +100,7 @@ export default class Sidebar extends React.Component {
|
||||
Object.keys(unreadCounts).forEach((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;
|
||||
mentions += unreadCounts[chId].mentions;
|
||||
}
|
||||
@@ -128,13 +132,19 @@ export default class Sidebar extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
onInChannelChange() {
|
||||
this.setState({inChannelChange: !this.state.inChannelChange});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ChannelStore.addChangeListener(this.onChange);
|
||||
UserStore.addChangeListener(this.onChange);
|
||||
UserStore.addInTeamChangeListener(this.onChange);
|
||||
UserStore.addInChannelChangeListener(this.onInChannelChange);
|
||||
UserStore.addStatusesChangeListener(this.onChange);
|
||||
TeamStore.addChangeListener(this.onChange);
|
||||
PreferenceStore.addChangeListener(this.onChange);
|
||||
ModalStore.addModalListener(ActionTypes.TOGGLE_DM_MODAL, this.onModalChange);
|
||||
|
||||
this.updateTitle();
|
||||
this.updateUnreadIndicators();
|
||||
@@ -179,13 +189,19 @@ export default class Sidebar extends React.Component {
|
||||
ChannelStore.removeChangeListener(this.onChange);
|
||||
UserStore.removeChangeListener(this.onChange);
|
||||
UserStore.removeInTeamChangeListener(this.onChange);
|
||||
UserStore.removeInChannelChangeListener(this.onChange);
|
||||
UserStore.removeStatusesChangeListener(this.onChange);
|
||||
TeamStore.removeChangeListener(this.onChange);
|
||||
PreferenceStore.removeChangeListener(this.onChange);
|
||||
ModalStore.removeModalListener(ActionTypes.TOGGLE_DM_MODAL, this.onModalChange);
|
||||
document.removeEventListener('keydown', this.navigateChannelShortcut);
|
||||
document.removeEventListener('keydown', this.navigateUnreadChannelShortcut);
|
||||
}
|
||||
|
||||
onModalChange(value, args) {
|
||||
this.showMoreDirectChannelsModal(args.startingUsers);
|
||||
}
|
||||
|
||||
onChange() {
|
||||
if (this.state.currentTeam.id !== TeamStore.getCurrentId()) {
|
||||
ChannelStore.clear();
|
||||
@@ -203,11 +219,13 @@ export default class Sidebar extends React.Component {
|
||||
}
|
||||
|
||||
let currentChannelName = channel.display_name;
|
||||
if (channel.type === 'D') {
|
||||
if (channel.type === Constants.DM_CHANNEL) {
|
||||
const teammate = Utils.getDirectTeammate(channel.id);
|
||||
if (teammate != null) {
|
||||
currentChannelName = teammate.username;
|
||||
}
|
||||
} else if (channel.type === Constants.GM_CHANNEL) {
|
||||
currentChannelName = ChannelUtils.buildGroupChannelName(channel.id);
|
||||
}
|
||||
|
||||
const unread = this.getTotalUnreadCount();
|
||||
@@ -331,7 +349,7 @@ export default class Sidebar extends React.Component {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -340,9 +358,19 @@ export default class Sidebar extends React.Component {
|
||||
if (!this.isLeaving.get(channel.id)) {
|
||||
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(
|
||||
Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
|
||||
channel.teammate_id,
|
||||
category,
|
||||
id,
|
||||
'false',
|
||||
() => {
|
||||
this.isLeaving.set(channel.id, false);
|
||||
@@ -382,13 +410,13 @@ export default class Sidebar extends React.Component {
|
||||
this.setState({newChannelModalType: ''});
|
||||
}
|
||||
|
||||
showMoreDirectChannelsModal() {
|
||||
showMoreDirectChannelsModal(startingUsers) {
|
||||
trackEvent('ui', 'ui_channels_more_direct');
|
||||
this.setState({showDirectChannelsModal: true});
|
||||
this.setState({showDirectChannelsModal: true, startingUsers});
|
||||
}
|
||||
|
||||
hideMoreDirectChannelsModal() {
|
||||
this.setState({showDirectChannelsModal: false});
|
||||
this.setState({showDirectChannelsModal: false, startingUsers: null});
|
||||
}
|
||||
|
||||
openLeftSidebar() {
|
||||
@@ -509,11 +537,16 @@ export default class Sidebar extends React.Component {
|
||||
rowClass += ' has-badge';
|
||||
}
|
||||
|
||||
let displayName = channel.display_name;
|
||||
|
||||
var icon = null;
|
||||
if (channel.type === Constants.OPEN_CHANNEL) {
|
||||
icon = <div className='status'><i className='fa fa-globe'/></div>;
|
||||
} else if (channel.type === Constants.PRIVATE_CHANNEL) {
|
||||
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 {
|
||||
// set up status icon for direct message channels (status is null for other channel types)
|
||||
icon = (
|
||||
@@ -576,7 +609,7 @@ export default class Sidebar extends React.Component {
|
||||
onClick={this.trackChannelSelectedEvent}
|
||||
>
|
||||
{icon}
|
||||
{channel.display_name}
|
||||
{displayName}
|
||||
{badge}
|
||||
{closeButton}
|
||||
</Link>
|
||||
@@ -615,27 +648,10 @@ export default class Sidebar extends React.Component {
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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
|
||||
if (this.lastBadgesActive !== this.badgesActive) {
|
||||
var link = document.createElement('link');
|
||||
@@ -659,7 +675,7 @@ export default class Sidebar extends React.Component {
|
||||
<li key='more'>
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.showMoreDirectChannelsModal}
|
||||
onClick={() => this.showMoreDirectChannelsModal()}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='sidebar.moreElips'
|
||||
@@ -753,6 +769,7 @@ export default class Sidebar extends React.Component {
|
||||
moreDirectChannelsModal = (
|
||||
<MoreDirectChannels
|
||||
onModalDismissed={this.hideMoreDirectChannelsModal}
|
||||
startingUsers={this.state.startingUsers}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -866,8 +883,6 @@ export default class Sidebar extends React.Component {
|
||||
</h4>
|
||||
</li>
|
||||
{directMessageItems}
|
||||
{directDivider}
|
||||
{directMessageNonTeamItems}
|
||||
{directMessageMore}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ export default class StatusIcon extends React.Component {
|
||||
|
||||
return (
|
||||
<span
|
||||
className='status'
|
||||
className={'status ' + this.props.className}
|
||||
dangerouslySetInnerHTML={{__html: statusIcon}}
|
||||
/>
|
||||
);
|
||||
@@ -41,7 +41,12 @@ export default class StatusIcon extends React.Component {
|
||||
|
||||
}
|
||||
|
||||
StatusIcon.defaultProps = {
|
||||
className: ''
|
||||
};
|
||||
|
||||
StatusIcon.propTypes = {
|
||||
status: React.PropTypes.string,
|
||||
className: 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 {Constants, ActionTypes} from 'utils/constants.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';
|
||||
|
||||
@@ -25,12 +25,15 @@ class SwitchChannelSuggestion extends Suggestion {
|
||||
className += ' suggestion--selected';
|
||||
}
|
||||
|
||||
const displayName = item.display_name;
|
||||
let displayName = item.display_name;
|
||||
let icon = null;
|
||||
if (item.type === Constants.OPEN_CHANNEL) {
|
||||
icon = <div className='status'><i className='fa fa-globe'/></div>;
|
||||
} else if (item.type === Constants.PRIVATE_CHANNEL) {
|
||||
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 {
|
||||
icon = (
|
||||
<div className='pull-left'>
|
||||
@@ -74,7 +77,11 @@ export default class SwitchChannelProvider extends Provider {
|
||||
for (const id of Object.keys(allChannels)) {
|
||||
const channel = allChannels[id];
|
||||
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.copyright": "Copyright 2016 Mattermost, Inc. All rights reserved",
|
||||
"about.database": "Database:",
|
||||
@@ -1638,6 +1644,8 @@
|
||||
"more_channels.prev": "Previous",
|
||||
"more_channels.title": "More Channels",
|
||||
"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.title": "Direct Messages",
|
||||
"msg_typing.areTyping": "{users} and {last} are typing...",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"react-dom": "15.4.2",
|
||||
"react-intl": "2.2.3",
|
||||
"react-router": "2.8.1",
|
||||
"react-select": "1.0.0-rc.3",
|
||||
"react-select": "1.0.0-rc.2",
|
||||
"superagent": "3.4.1",
|
||||
"twemoji": "2.2.3",
|
||||
"velocity-animate": "1.4.2",
|
||||
|
||||
@@ -21,7 +21,7 @@ import BrowserStore from 'stores/browser_store.jsx';
|
||||
import emojiRoute from 'routes/route_emoji.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) {
|
||||
doChannelChange(nextState, replace, callback);
|
||||
@@ -36,6 +36,8 @@ function doChannelChange(state, replace, callback) {
|
||||
|
||||
if (channel && channel.type === Constants.DM_CHANNEL) {
|
||||
loadNewDMIfNeeded(Utils.getUserIdFromChannelName(channel));
|
||||
} else if (channel && channel.type === Constants.GM_CHANNEL) {
|
||||
loadNewGMIfNeeded(channel.id);
|
||||
}
|
||||
|
||||
if (!channel) {
|
||||
@@ -120,7 +122,7 @@ function preNeedsTeam(nextState, replace, callback) {
|
||||
});
|
||||
|
||||
loadStatusesForChannelAndSidebar();
|
||||
loadProfilesAndTeamMembersForDMSidebar();
|
||||
loadProfilesForSidebar();
|
||||
|
||||
d1.resolve();
|
||||
},
|
||||
|
||||
@@ -170,6 +170,10 @@
|
||||
float: left;
|
||||
font-size: 17px;
|
||||
line-height: 27px;
|
||||
max-width: calc(100% - 80px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
.name {
|
||||
color: $white;
|
||||
@@ -479,6 +483,10 @@
|
||||
.filter-row {
|
||||
margin: 5px 0 10px;
|
||||
width: 300px;
|
||||
|
||||
&.filter-row--full {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.member-count {
|
||||
@@ -582,6 +590,24 @@
|
||||
border-bottom: 1px solid $light-gray;
|
||||
display: flex;
|
||||
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 {
|
||||
@@ -611,6 +637,7 @@
|
||||
|
||||
.more-modal__list {
|
||||
flex-grow: 500;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.filter-button {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
@import 'links';
|
||||
@import 'mentions';
|
||||
@import 'modal';
|
||||
@import 'multi-select';
|
||||
@import 'oauth';
|
||||
@import 'popover';
|
||||
@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;
|
||||
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 {
|
||||
max-height: 14px;
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
|
||||
&.dropdown {
|
||||
float: left;
|
||||
max-width: 100%;
|
||||
max-width: 90%;
|
||||
padding-right: 1em;
|
||||
|
||||
.header-dropdown__icon {
|
||||
@@ -182,7 +182,6 @@
|
||||
}
|
||||
|
||||
.channel-intro-profile {
|
||||
margin-left: 63px;
|
||||
margin-top: 5px;
|
||||
|
||||
.user-popover {
|
||||
@@ -193,7 +192,10 @@
|
||||
}
|
||||
|
||||
.channel-intro-img {
|
||||
float: left;
|
||||
.status-wrapper {
|
||||
height: 50px;
|
||||
margin: 0 10px 10px 0;
|
||||
}
|
||||
|
||||
img {
|
||||
@include border-radius(100px);
|
||||
@@ -211,10 +213,6 @@
|
||||
background: $bg--gray;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.channel-intro-text {
|
||||
margin-top: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
// Team Header in Sidebar
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
@charset 'UTF-8';
|
||||
|
||||
@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 {
|
||||
padding-bottom: 10px;
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ class ModalStoreClass extends EventEmitter {
|
||||
case ActionTypes.TOGGLE_GET_POST_LINK_MODAL:
|
||||
case ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL:
|
||||
case ActionTypes.TOGGLE_GET_PUBLIC_LINK_MODAL:
|
||||
case ActionTypes.TOGGLE_DM_MODAL:
|
||||
this.emit(type, value, args);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import UserStore from './user_store.jsx';
|
||||
import ChannelStore from './channel_store.jsx';
|
||||
import * as UserAgent from 'utils/user_agent.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import {buildGroupChannelName} from 'utils/channel_utils.jsx';
|
||||
import * as PostUtils from 'utils/post_utils.jsx';
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
|
||||
@@ -74,6 +75,8 @@ class NotificationStoreClass extends EventEmitter {
|
||||
};
|
||||
} else if (channel.type === Constants.DM_CHANNEL) {
|
||||
title = Utils.localizeMessage('notification.dm', 'Direct Message');
|
||||
} else if (channel.type === Constants.GM_CHANNEL) {
|
||||
title = buildGroupChannelName(channel.id);
|
||||
} else {
|
||||
title = channel.display_name;
|
||||
}
|
||||
|
||||
@@ -414,13 +414,18 @@ class UserStoreClass extends EventEmitter {
|
||||
userIds.splice(index, 1);
|
||||
}
|
||||
|
||||
getProfileListInChannel(channelId = ChannelStore.getCurrentId()) {
|
||||
getProfileListInChannel(channelId = ChannelStore.getCurrentId(), skipCurrent = false) {
|
||||
const userIds = this.profiles_in_channel[channelId] || [];
|
||||
const currentId = this.getCurrentId();
|
||||
const profiles = [];
|
||||
|
||||
for (let i = 0; i < userIds.length; i++) {
|
||||
const profile = this.getProfile(userIds[i]);
|
||||
if (profile) {
|
||||
if (skipCurrent && profile.id === currentId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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) {
|
||||
TestHelper.initBasic(() => {
|
||||
var channel = TestHelper.basicChannel();
|
||||
|
||||
@@ -23,8 +23,10 @@ export function createChannelIntroMessage(channel, fullWidthIntro) {
|
||||
centeredIntro = 'channel-intro--centered';
|
||||
}
|
||||
|
||||
if (channel.type === 'D') {
|
||||
if (channel.type === Constants.DM_CHANNEL) {
|
||||
return createDMIntroMessage(channel, centeredIntro);
|
||||
} else if (channel.type === Constants.GM_CHANNEL) {
|
||||
return createGMIntroMessage(channel, centeredIntro);
|
||||
} else if (ChannelStore.isDefault(channel)) {
|
||||
return createDefaultIntroMessage(channel, centeredIntro);
|
||||
} else if (channel.name === Constants.OFFTOPIC_CHANNEL) {
|
||||
@@ -35,6 +37,65 @@ export function createChannelIntroMessage(channel, fullWidthIntro) {
|
||||
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) {
|
||||
var teammate = Utils.getDirectTeammate(channel.id);
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ const Preferences = Constants.Preferences;
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
import PreferenceStore from 'stores/preference_store.jsx';
|
||||
import LocalizationStore from 'stores/localization_store.jsx';
|
||||
|
||||
@@ -15,30 +14,28 @@ import LocalizationStore from 'stores/localization_store.jsx';
|
||||
* Example: {
|
||||
* publicChannels: [...],
|
||||
* privateChannels: [...],
|
||||
* directChannels: [...],
|
||||
* directNonTeamChannels: [...],
|
||||
* directAndGroupChannels: [...],
|
||||
* favoriteChannels: [...]
|
||||
* }
|
||||
*/
|
||||
export function buildDisplayableChannelList(persistentChannels) {
|
||||
const missingDMChannels = createMissingDirectChannels(persistentChannels);
|
||||
const missingDirectChannels = createMissingDirectChannels(persistentChannels);
|
||||
|
||||
const channels = persistentChannels.
|
||||
concat(missingDMChannels).
|
||||
concat(missingDirectChannels).
|
||||
map(completeDirectChannelInfo).
|
||||
filter(isNotDeletedChannel).
|
||||
sort(sortChannelsByDisplayName);
|
||||
|
||||
const favoriteChannels = channels.filter(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 {
|
||||
favoriteChannels,
|
||||
publicChannels: notFavoriteChannels.filter(isOpenChannel),
|
||||
privateChannels: notFavoriteChannels.filter(isPrivateChannel),
|
||||
directChannels: directChannels.filter(isConnectedToTeamMember),
|
||||
directNonTeamChannels: directChannels.filter(isNotConnectedToTeamMember)
|
||||
directAndGroupChannels
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,6 +59,14 @@ export function isPrivateChannel(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) {
|
||||
return channel.type === Constants.DM_CHANNEL;
|
||||
}
|
||||
@@ -88,12 +93,12 @@ export function completeDirectChannelInfo(channel) {
|
||||
}
|
||||
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -186,6 +191,19 @@ export function showDeleteOption(channel, isAdmin, isSystemAdmin, isChannelAdmin
|
||||
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
|
||||
*/
|
||||
@@ -215,22 +233,14 @@ function createFakeChannelCurried(userId) {
|
||||
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) {
|
||||
return (...args) => !f(...args);
|
||||
}
|
||||
|
||||
function orX(...fns) {
|
||||
return (...args) => fns.some((f) => f(...args));
|
||||
}
|
||||
|
||||
function andX(...fns) {
|
||||
return (...args) => fns.every((f) => f(...args));
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import windows10ThemeImage from 'images/themes/windows_dark.png';
|
||||
|
||||
export const Preferences = {
|
||||
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
|
||||
CATEGORY_GROUP_CHANNEL_SHOW: 'group_channel_show',
|
||||
CATEGORY_DISPLAY_SETTINGS: 'display_settings',
|
||||
DISPLAY_PREFER_NICKNAME: 'nickname_full_name',
|
||||
DISPLAY_PREFER_FULL_NAME: 'full_name',
|
||||
@@ -164,6 +165,7 @@ export const ActionTypes = keyMirror({
|
||||
TOGGLE_GET_POST_LINK_MODAL: null,
|
||||
TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null,
|
||||
TOGGLE_GET_PUBLIC_LINK_MODAL: null,
|
||||
TOGGLE_DM_MODAL: null,
|
||||
|
||||
SUGGESTION_PRETEXT_CHANGED: 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'],
|
||||
MAX_DMS: 20,
|
||||
MAX_USERS_IN_GM: 8,
|
||||
MIN_USERS_IN_GM: 3,
|
||||
MAX_CHANNEL_POPOVER_COUNT: 100,
|
||||
DM_CHANNEL: 'D',
|
||||
GM_CHANNEL: 'G',
|
||||
OPEN_CHANNEL: 'O',
|
||||
PRIVATE_CHANNEL: 'P',
|
||||
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 .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.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){.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) {
|
||||
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 .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));
|
||||
@@ -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('.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('.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('.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));
|
||||
|
||||
Ссылка в новой задаче
Block a user