Mark category as read (#24003)
* Mark category as read * Fix lint and test * Fix tests * Fix test and remove wrong aria * Address server issues and add mark as read for unreads * Missing changes * Fix tests * fix tests * Add confirmation popup to mark as read category * Always use viewMultipleChannels and other fixes * Remove unneeded code * Fix test * Address feedback * Address feedback * Fix tests * Fix test * Fix tests * Update aria-haspopup depending on the number of channels to mark as viewed --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
c1c07ba1bb
Коммит
e9b3afecc2
@@ -24,6 +24,7 @@ func (api *API) InitChannel() {
|
|||||||
api.BaseRoutes.Channels.Handle("/group/search", api.APISessionRequiredDisableWhenBusy(searchGroupChannels)).Methods("POST")
|
api.BaseRoutes.Channels.Handle("/group/search", api.APISessionRequiredDisableWhenBusy(searchGroupChannels)).Methods("POST")
|
||||||
api.BaseRoutes.Channels.Handle("/group", api.APISessionRequired(createGroupChannel)).Methods("POST")
|
api.BaseRoutes.Channels.Handle("/group", api.APISessionRequired(createGroupChannel)).Methods("POST")
|
||||||
api.BaseRoutes.Channels.Handle("/members/{user_id:[A-Za-z0-9]+}/view", api.APISessionRequired(viewChannel)).Methods("POST")
|
api.BaseRoutes.Channels.Handle("/members/{user_id:[A-Za-z0-9]+}/view", api.APISessionRequired(viewChannel)).Methods("POST")
|
||||||
|
api.BaseRoutes.Channels.Handle("/members/{user_id:[A-Za-z0-9]+}/mark_read", api.APISessionRequired(readMultipleChannels)).Methods("POST")
|
||||||
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/scheme", api.APISessionRequired(updateChannelScheme)).Methods("PUT")
|
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/scheme", api.APISessionRequired(updateChannelScheme)).Methods("PUT")
|
||||||
api.BaseRoutes.Channels.Handle("/stats/member_count", api.APISessionRequired(getChannelsMemberCount)).Methods("POST")
|
api.BaseRoutes.Channels.Handle("/stats/member_count", api.APISessionRequired(getChannelsMemberCount)).Methods("POST")
|
||||||
|
|
||||||
@@ -1537,6 +1538,32 @@ func viewChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readMultipleChannels(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
c.RequireUserId()
|
||||||
|
|
||||||
|
var channelIDs []string
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&channelIDs)
|
||||||
|
if err != nil || len(channelIDs) == 0 {
|
||||||
|
c.SetInvalidParamWithErr("channel_ids", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
times, appErr := c.App.MarkChannelsAsViewed(c.AppContext, channelIDs, c.Params.UserId, c.AppContext.Session().Id, true, c.App.IsCRTEnabledForUser(c.AppContext, c.Params.UserId))
|
||||||
|
if appErr != nil {
|
||||||
|
c.Err = appErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &model.ChannelViewResponse{
|
||||||
|
Status: "OK",
|
||||||
|
LastViewedAtTimes: times,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||||
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func updateChannelMemberRoles(c *Context, w http.ResponseWriter, r *http.Request) {
|
func updateChannelMemberRoles(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
c.RequireChannelId().RequireUserId()
|
c.RequireChannelId().RequireUserId()
|
||||||
if c.Err != nil {
|
if c.Err != nil {
|
||||||
|
|||||||
@@ -2478,9 +2478,10 @@ func TestViewChannel(t *testing.T) {
|
|||||||
CheckBadRequestStatus(t, resp)
|
CheckBadRequestStatus(t, resp)
|
||||||
|
|
||||||
view.ChannelId = "correctlysizedjunkdddfdfdf"
|
view.ChannelId = "correctlysizedjunkdddfdfdf"
|
||||||
_, resp, err = client.ViewChannel(context.Background(), th.BasicUser.Id, view)
|
viewResult, _, err := client.ViewChannel(context.Background(), th.BasicUser.Id, view)
|
||||||
require.Error(t, err)
|
require.NoError(t, err)
|
||||||
CheckBadRequestStatus(t, resp)
|
require.Len(t, viewResult.LastViewedAtTimes, 0)
|
||||||
|
|
||||||
view.ChannelId = th.BasicChannel.Id
|
view.ChannelId = th.BasicChannel.Id
|
||||||
|
|
||||||
member, _, err := client.GetChannelMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id, "")
|
member, _, err := client.GetChannelMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id, "")
|
||||||
|
|||||||
@@ -2970,57 +2970,32 @@ func (a *App) SearchChannelsUserNotIn(c request.CTX, teamID string, userID strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) MarkChannelsAsViewed(c request.CTX, channelIDs []string, userID string, currentSessionId string, collapsedThreadsSupported, isCRTEnabled bool) (map[string]int64, *model.AppError) {
|
func (a *App) MarkChannelsAsViewed(c request.CTX, channelIDs []string, userID string, currentSessionId string, collapsedThreadsSupported, isCRTEnabled bool) (map[string]int64, *model.AppError) {
|
||||||
// I start looking for channels with notifications before I mark it as read, to clear the push notifications if needed
|
var err error
|
||||||
channelsToClearPushNotifications := []string{}
|
|
||||||
if a.canSendPushNotifications() {
|
|
||||||
for _, channelID := range channelIDs {
|
|
||||||
channel, errCh := a.Srv().Store().Channel().Get(channelID, true)
|
|
||||||
if errCh != nil {
|
|
||||||
c.Logger().Warn("Failed to get channel", mlog.Err(errCh))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
member, err := a.Srv().Store().Channel().GetMember(context.Background(), channelID, userID)
|
user, err := a.Srv().Store().User().Get(c.Context(), userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Logger().Warn("Failed to get membership", mlog.Err(err))
|
return nil, model.NewAppError("MarkChannelsAsViewed", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
continue
|
}
|
||||||
}
|
|
||||||
|
// We use channelsToView to later only update those, or early return if no channel is to be read
|
||||||
notify := member.NotifyProps[model.PushNotifyProp]
|
channelsToView, channelsToClearPushNotifications, times, err := a.Srv().Store().Channel().GetChannelsWithUnreadsAndWithMentions(c.Context(), channelIDs, userID, user.NotifyProps)
|
||||||
if notify == model.ChannelNotifyDefault {
|
if err != nil {
|
||||||
user, err := a.GetUser(userID)
|
return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.get_channels_with_unreads_and_with_mentions.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
if err != nil {
|
}
|
||||||
c.Logger().Warn("Failed to get user", mlog.String("user_id", userID), mlog.Err(err))
|
|
||||||
continue
|
if len(channelsToView) == 0 {
|
||||||
}
|
return times, nil
|
||||||
notify = user.NotifyProps[model.PushNotifyProp]
|
|
||||||
}
|
|
||||||
if notify == model.UserNotifyAll {
|
|
||||||
if count, err := a.Srv().Store().User().GetAnyUnreadPostCountForChannel(userID, channelID); err == nil {
|
|
||||||
if count > 0 {
|
|
||||||
channelsToClearPushNotifications = append(channelsToClearPushNotifications, channelID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if notify == model.UserNotifyMention || channel.Type == model.ChannelTypeDirect {
|
|
||||||
if count, err := a.Srv().Store().User().GetUnreadCountForChannel(userID, channelID); err == nil {
|
|
||||||
if count > 0 {
|
|
||||||
channelsToClearPushNotifications = append(channelsToClearPushNotifications, channelID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
updateThreads := *a.Config().ServiceSettings.ThreadAutoFollow && (!collapsedThreadsSupported || !isCRTEnabled)
|
updateThreads := *a.Config().ServiceSettings.ThreadAutoFollow && (!collapsedThreadsSupported || !isCRTEnabled)
|
||||||
if updateThreads {
|
if updateThreads {
|
||||||
err = a.Srv().Store().Thread().MarkAllAsReadByChannels(userID, channelIDs)
|
err = a.Srv().Store().Thread().MarkAllAsReadByChannels(userID, channelsToView)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.update_last_viewed_at.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
return nil, model.NewAppError("MarkChannelsAsViewed", "app.thread.mark_all_as_read_by_channels.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
times, err := a.Srv().Store().Channel().UpdateLastViewedAt(channelIDs, userID)
|
_, err = a.Srv().Store().Channel().UpdateLastViewedAt(channelsToView, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var invErr *store.ErrInvalidInput
|
var invErr *store.ErrInvalidInput
|
||||||
switch {
|
switch {
|
||||||
@@ -3032,19 +3007,18 @@ func (a *App) MarkChannelsAsViewed(c request.CTX, channelIDs []string, userID st
|
|||||||
}
|
}
|
||||||
|
|
||||||
if *a.Config().ServiceSettings.EnableChannelViewedMessages {
|
if *a.Config().ServiceSettings.EnableChannelViewedMessages {
|
||||||
for _, channelID := range channelIDs {
|
message := model.NewWebSocketEvent(model.WebsocketEventMultipleChannelsViewed, "", "", userID, nil, "")
|
||||||
message := model.NewWebSocketEvent(model.WebsocketEventChannelViewed, "", "", userID, nil, "")
|
message.Add("channel_times", times)
|
||||||
message.Add("channel_id", channelID)
|
a.Publish(message)
|
||||||
a.Publish(message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, channelID := range channelsToClearPushNotifications {
|
for _, channelID := range channelsToClearPushNotifications {
|
||||||
a.clearPushNotification(currentSessionId, userID, channelID, "")
|
a.clearPushNotification(currentSessionId, userID, channelID, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
if updateThreads && isCRTEnabled {
|
if updateThreads && isCRTEnabled {
|
||||||
timestamp := model.GetMillis()
|
timestamp := model.GetMillis()
|
||||||
for _, channelID := range channelIDs {
|
for _, channelID := range channelsToView {
|
||||||
message := model.NewWebSocketEvent(model.WebsocketEventThreadReadChanged, "", channelID, userID, nil, "")
|
message := model.NewWebSocketEvent(model.WebsocketEventThreadReadChanged, "", channelID, userID, nil, "")
|
||||||
message.Add("timestamp", timestamp)
|
message.Add("timestamp", timestamp)
|
||||||
a.Publish(message)
|
a.Publish(message)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -18,7 +17,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
"github.com/mattermost/mattermost/server/v8/channels/app/users"
|
|
||||||
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
|
"github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2107,48 +2105,6 @@ func TestPatchChannelModerationsForChannel(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMarkChannelsAsViewedPanic verifies that returning an error from a.GetUser
|
|
||||||
// does not cause a panic.
|
|
||||||
func TestMarkChannelsAsViewedPanic(t *testing.T) {
|
|
||||||
th := SetupWithStoreMock(t)
|
|
||||||
defer th.TearDown()
|
|
||||||
|
|
||||||
mockStore := th.App.Srv().Store().(*mocks.Store)
|
|
||||||
mockUserStore := mocks.UserStore{}
|
|
||||||
mockUserStore.On("Get", context.Background(), "userID").Return(nil, model.NewAppError("SqlUserStore.Get", "app.user.get.app_error", nil, "user_id=userID", http.StatusInternalServerError))
|
|
||||||
mockChannelStore := mocks.ChannelStore{}
|
|
||||||
mockChannelStore.On("Get", "channelID", true).Return(&model.Channel{}, nil)
|
|
||||||
mockChannelStore.On("GetMember", context.Background(), "channelID", "userID").Return(&model.ChannelMember{
|
|
||||||
NotifyProps: model.StringMap{
|
|
||||||
model.PushNotifyProp: model.ChannelNotifyDefault,
|
|
||||||
}}, nil)
|
|
||||||
times := map[string]int64{
|
|
||||||
"userID": 1,
|
|
||||||
}
|
|
||||||
mockChannelStore.On("UpdateLastViewedAt", []string{"channelID"}, "userID").Return(times, nil)
|
|
||||||
mockSessionStore := mocks.SessionStore{}
|
|
||||||
mockOAuthStore := mocks.OAuthStore{}
|
|
||||||
var err error
|
|
||||||
th.App.ch.srv.userService, err = users.New(users.ServiceConfig{
|
|
||||||
UserStore: &mockUserStore,
|
|
||||||
SessionStore: &mockSessionStore,
|
|
||||||
OAuthStore: &mockOAuthStore,
|
|
||||||
ConfigFn: th.App.ch.srv.platform.Config,
|
|
||||||
LicenseFn: th.App.ch.srv.License,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
mockPreferenceStore := mocks.PreferenceStore{}
|
|
||||||
mockPreferenceStore.On("Get", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&model.Preference{Value: "test"}, nil)
|
|
||||||
mockStore.On("Channel").Return(&mockChannelStore)
|
|
||||||
mockStore.On("Preference").Return(&mockPreferenceStore)
|
|
||||||
mockThreadStore := mocks.ThreadStore{}
|
|
||||||
mockThreadStore.On("MarkAllAsReadByChannels", "userID", []string{"channelID"}).Return(nil)
|
|
||||||
mockStore.On("Thread").Return(&mockThreadStore)
|
|
||||||
|
|
||||||
_, appErr := th.App.MarkChannelsAsViewed(th.Context, []string{"channelID"}, "userID", th.Context.Session().Id, false, false)
|
|
||||||
require.Nil(t, appErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClearChannelMembersCache(t *testing.T) {
|
func TestClearChannelMembersCache(t *testing.T) {
|
||||||
th := SetupWithStoreMock(t)
|
th := SetupWithStoreMock(t)
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|||||||
@@ -726,7 +726,7 @@ func (wc *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool {
|
|||||||
switch msg.EventType() {
|
switch msg.EventType() {
|
||||||
case model.WebsocketEventTyping,
|
case model.WebsocketEventTyping,
|
||||||
model.WebsocketEventStatusChange,
|
model.WebsocketEventStatusChange,
|
||||||
model.WebsocketEventChannelViewed:
|
model.WebsocketEventMultipleChannelsViewed:
|
||||||
if time.Since(wc.lastLogTimeSlow) > websocketSuppressWarnThreshold {
|
if time.Since(wc.lastLogTimeSlow) > websocketSuppressWarnThreshold {
|
||||||
mlog.Warn(
|
mlog.Warn(
|
||||||
"websocket.slow: dropping message",
|
"websocket.slow: dropping message",
|
||||||
|
|||||||
@@ -1113,26 +1113,6 @@ func TestCreatePostAsUser(t *testing.T) {
|
|||||||
require.Equal(t, channelMemberAfter.LastViewedAt, channelMemberBefore.LastViewedAt)
|
require.Equal(t, channelMemberAfter.LastViewedAt, channelMemberBefore.LastViewedAt)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("logs warning for user not in channel", func(t *testing.T) {
|
|
||||||
th := Setup(t).InitBasic()
|
|
||||||
defer th.TearDown()
|
|
||||||
user := th.CreateUser()
|
|
||||||
th.LinkUserToTeam(user, th.BasicTeam)
|
|
||||||
|
|
||||||
post := &model.Post{
|
|
||||||
ChannelId: th.BasicChannel.Id,
|
|
||||||
Message: "test",
|
|
||||||
UserId: user.Id,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, appErr := th.App.CreatePostAsUser(th.Context, post, "", true)
|
|
||||||
require.Nil(t, appErr)
|
|
||||||
|
|
||||||
require.NoError(t, th.TestLogger.Flush())
|
|
||||||
|
|
||||||
testlib.AssertLog(t, th.LogBuffer, mlog.LvlWarn.Name, "Failed to get membership")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("does not log warning for bot user not in channel", func(t *testing.T) {
|
t.Run("does not log warning for bot user not in channel", func(t *testing.T) {
|
||||||
th := Setup(t).InitBasic()
|
th := Setup(t).InitBasic()
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|||||||
@@ -1322,6 +1322,24 @@ func (s *OpenTracingLayerChannelStore) GetChannelsWithTeamDataByIds(channelIds [
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *OpenTracingLayerChannelStore) GetChannelsWithUnreadsAndWithMentions(ctx context.Context, channelIDs []string, userID string, userNotifyProps model.StringMap) ([]string, []string, map[string]int64, error) {
|
||||||
|
origCtx := s.Root.Store.Context()
|
||||||
|
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsWithUnreadsAndWithMentions")
|
||||||
|
s.Root.Store.SetContext(newCtx)
|
||||||
|
defer func() {
|
||||||
|
s.Root.Store.SetContext(origCtx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer span.Finish()
|
||||||
|
result, resultVar1, resultVar2, err := s.ChannelStore.GetChannelsWithUnreadsAndWithMentions(ctx, channelIDs, userID, userNotifyProps)
|
||||||
|
if err != nil {
|
||||||
|
span.LogFields(spanlog.Error(err))
|
||||||
|
ext.Error.Set(span, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, resultVar1, resultVar2, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *OpenTracingLayerChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) {
|
func (s *OpenTracingLayerChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) {
|
||||||
origCtx := s.Root.Store.Context()
|
origCtx := s.Root.Store.Context()
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetDeleted")
|
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetDeleted")
|
||||||
|
|||||||
@@ -1468,6 +1468,27 @@ func (s *RetryLayerChannelStore) GetChannelsWithTeamDataByIds(channelIds []strin
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RetryLayerChannelStore) GetChannelsWithUnreadsAndWithMentions(ctx context.Context, channelIDs []string, userID string, userNotifyProps model.StringMap) ([]string, []string, map[string]int64, error) {
|
||||||
|
|
||||||
|
tries := 0
|
||||||
|
for {
|
||||||
|
result, resultVar1, resultVar2, err := s.ChannelStore.GetChannelsWithUnreadsAndWithMentions(ctx, channelIDs, userID, userNotifyProps)
|
||||||
|
if err == nil {
|
||||||
|
return result, resultVar1, resultVar2, nil
|
||||||
|
}
|
||||||
|
if !isRepeatableError(err) {
|
||||||
|
return result, resultVar1, resultVar2, err
|
||||||
|
}
|
||||||
|
tries++
|
||||||
|
if tries >= 3 {
|
||||||
|
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||||
|
return result, resultVar1, resultVar2, err
|
||||||
|
}
|
||||||
|
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (s *RetryLayerChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) {
|
func (s *RetryLayerChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) {
|
||||||
|
|
||||||
tries := 0
|
tries := 0
|
||||||
|
|||||||
@@ -2020,6 +2020,82 @@ func (s SqlChannelStore) GetChannelMembersTimezones(channelId string) ([]model.S
|
|||||||
return dbMembersTimezone, nil
|
return dbMembersTimezone, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SqlChannelStore) GetChannelsWithUnreadsAndWithMentions(ctx context.Context, channelIDs []string, userID string, userNotifyProps model.StringMap) ([]string, []string, map[string]int64, error) {
|
||||||
|
query := s.getQueryBuilder().Select(
|
||||||
|
"Channels.Id",
|
||||||
|
"Channels.Type",
|
||||||
|
"Channels.TotalMsgCount",
|
||||||
|
"Channels.LastPostAt",
|
||||||
|
"ChannelMembers.MsgCount",
|
||||||
|
"ChannelMembers.MentionCount",
|
||||||
|
"ChannelMembers.NotifyProps",
|
||||||
|
"ChannelMembers.LastViewedAt",
|
||||||
|
).
|
||||||
|
From("ChannelMembers").
|
||||||
|
InnerJoin("Channels ON ChannelMembers.ChannelId = Channels.Id").
|
||||||
|
Where(sq.Eq{
|
||||||
|
"ChannelMembers.ChannelId": channelIDs,
|
||||||
|
"ChannelMembers.UserId": userID,
|
||||||
|
})
|
||||||
|
|
||||||
|
queryString, args, err := query.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, errors.Wrap(err, "channel_tosql")
|
||||||
|
}
|
||||||
|
|
||||||
|
var channels []struct {
|
||||||
|
Id string
|
||||||
|
Type string
|
||||||
|
TotalMsgCount int
|
||||||
|
LastPostAt int64
|
||||||
|
MsgCount int
|
||||||
|
MentionCount int
|
||||||
|
NotifyProps model.StringMap
|
||||||
|
LastViewedAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.GetReplicaX().Select(&channels, queryString, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, errors.Wrap(err, "failed to find channels with unreads and with mentions data")
|
||||||
|
}
|
||||||
|
|
||||||
|
channelsWithUnreads := []string{}
|
||||||
|
channelsWithMentions := []string{}
|
||||||
|
readTimes := map[string]int64{}
|
||||||
|
|
||||||
|
for i := range channels {
|
||||||
|
channel := channels[i]
|
||||||
|
hasMentions := (channel.MentionCount > 0)
|
||||||
|
hasUnreads := (channel.TotalMsgCount-channel.MsgCount > 0) || hasMentions
|
||||||
|
|
||||||
|
if hasUnreads {
|
||||||
|
channelsWithUnreads = append(channelsWithUnreads, channel.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
notify := channel.NotifyProps[model.PushNotifyProp]
|
||||||
|
if notify == model.ChannelNotifyDefault {
|
||||||
|
notify = userNotifyProps[model.PushNotifyProp]
|
||||||
|
}
|
||||||
|
if notify == model.UserNotifyAll || channel.Type == string(model.ChannelTypeDirect) {
|
||||||
|
if hasUnreads {
|
||||||
|
channelsWithMentions = append(channelsWithMentions, channel.Id)
|
||||||
|
}
|
||||||
|
} else if notify == model.UserNotifyMention {
|
||||||
|
if hasMentions {
|
||||||
|
channelsWithMentions = append(channelsWithMentions, channel.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel.LastPostAt > channel.LastViewedAt {
|
||||||
|
readTimes[channel.Id] = channel.LastPostAt
|
||||||
|
} else {
|
||||||
|
readTimes[channel.Id] = channel.LastViewedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return channelsWithUnreads, channelsWithMentions, readTimes, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s SqlChannelStore) GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error) {
|
func (s SqlChannelStore) GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error) {
|
||||||
selectSQL, args, err := s.channelMembersForTeamWithSchemeSelectQuery.
|
selectSQL, args, err := s.channelMembersForTeamWithSchemeSelectQuery.
|
||||||
Where(sq.Eq{
|
Where(sq.Eq{
|
||||||
@@ -2512,6 +2588,10 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string)
|
|||||||
TotalMsgCountRoot int64
|
TotalMsgCountRoot int64
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
|
if len(channelIds) == 0 {
|
||||||
|
return map[string]int64{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// We use the question placeholder format for both databases, because
|
// We use the question placeholder format for both databases, because
|
||||||
// we replace that with the dollar format later on.
|
// we replace that with the dollar format later on.
|
||||||
// It's needed to support the prefix CTE query. See: https://github.com/Masterminds/squirrel/issues/285.
|
// It's needed to support the prefix CTE query. See: https://github.com/Masterminds/squirrel/issues/285.
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ type ChannelStore interface {
|
|||||||
GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error)
|
GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error)
|
||||||
AnalyticsDeletedTypeCount(teamID string, channelType model.ChannelType) (int64, error)
|
AnalyticsDeletedTypeCount(teamID string, channelType model.ChannelType) (int64, error)
|
||||||
GetChannelUnread(channelID, userID string) (*model.ChannelUnread, error)
|
GetChannelUnread(channelID, userID string) (*model.ChannelUnread, error)
|
||||||
|
GetChannelsWithUnreadsAndWithMentions(ctx context.Context, channelIDs []string, userID string, userNotifyProps model.StringMap) ([]string, []string, map[string]int64, error)
|
||||||
ClearCaches()
|
ClearCaches()
|
||||||
ClearMembersForUserCache()
|
ClearMembersForUserCache()
|
||||||
GetChannelsByScheme(schemeID string, offset int, limit int) (model.ChannelList, error)
|
GetChannelsByScheme(schemeID string, offset int, limit int) (model.ChannelList, error)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -149,6 +150,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlStore) {
|
|||||||
t.Run("UpdateSidebarChannelsByPreferences", func(t *testing.T) { testUpdateSidebarChannelsByPreferences(t, ss) })
|
t.Run("UpdateSidebarChannelsByPreferences", func(t *testing.T) { testUpdateSidebarChannelsByPreferences(t, ss) })
|
||||||
t.Run("SetShared", func(t *testing.T) { testSetShared(t, ss) })
|
t.Run("SetShared", func(t *testing.T) { testSetShared(t, ss) })
|
||||||
t.Run("GetTeamForChannel", func(t *testing.T) { testGetTeamForChannel(t, ss) })
|
t.Run("GetTeamForChannel", func(t *testing.T) { testGetTeamForChannel(t, ss) })
|
||||||
|
t.Run("GetChannelsWithUnreadsAndWithMentions", func(t *testing.T) { testGetChannelsWithUnreadsAndWithMentions(t, ss) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func testChannelStoreSave(t *testing.T, ss store.Store) {
|
func testChannelStoreSave(t *testing.T, ss store.Store) {
|
||||||
@@ -8065,3 +8067,166 @@ func testGetTeamForChannel(t *testing.T, ss store.Store) {
|
|||||||
var nfErr *store.ErrNotFound
|
var nfErr *store.ErrNotFound
|
||||||
require.True(t, errors.As(err, &nfErr))
|
require.True(t, errors.As(err, &nfErr))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testGetChannelsWithUnreadsAndWithMentions(t *testing.T, ss store.Store) {
|
||||||
|
setupMembership := func(
|
||||||
|
pushProp string,
|
||||||
|
withUnreads bool,
|
||||||
|
withMentions bool,
|
||||||
|
isDirect bool,
|
||||||
|
userId string,
|
||||||
|
) (model.Channel, model.ChannelMember) {
|
||||||
|
if !isDirect {
|
||||||
|
o1 := model.Channel{}
|
||||||
|
o1.TeamId = model.NewId()
|
||||||
|
o1.DisplayName = "Channel1"
|
||||||
|
o1.Name = NewTestId()
|
||||||
|
o1.Type = model.ChannelTypeOpen
|
||||||
|
o1.TotalMsgCount = 25
|
||||||
|
o1.LastPostAt = 12345
|
||||||
|
o1.LastRootPostAt = 12345
|
||||||
|
_, nErr := ss.Channel().Save(&o1, -1)
|
||||||
|
require.NoError(t, nErr)
|
||||||
|
|
||||||
|
m1 := model.ChannelMember{}
|
||||||
|
m1.ChannelId = o1.Id
|
||||||
|
m1.UserId = userId
|
||||||
|
m1.NotifyProps = model.GetDefaultChannelNotifyProps()
|
||||||
|
m1.NotifyProps[model.PushNotifyProp] = pushProp
|
||||||
|
if !withUnreads {
|
||||||
|
m1.MsgCount = o1.TotalMsgCount
|
||||||
|
m1.LastViewedAt = o1.LastPostAt
|
||||||
|
}
|
||||||
|
if withMentions {
|
||||||
|
m1.MentionCount = 5
|
||||||
|
}
|
||||||
|
_, err := ss.Channel().SaveMember(&m1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return o1, m1
|
||||||
|
}
|
||||||
|
|
||||||
|
o1, err := ss.Channel().CreateDirectChannel(&model.User{Id: userId}, &model.User{Id: model.NewId()}, func(channel *model.Channel) {
|
||||||
|
channel.TotalMsgCount = 25
|
||||||
|
channel.LastPostAt = 12345
|
||||||
|
channel.LastRootPostAt = 12345
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
m1, err := ss.Channel().GetMember(context.Background(), o1.Id, userId)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if !withUnreads {
|
||||||
|
m1.MsgCount = o1.TotalMsgCount
|
||||||
|
m1.LastViewedAt = o1.LastPostAt
|
||||||
|
}
|
||||||
|
if withMentions {
|
||||||
|
m1.MentionCount = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
m1, err = ss.Channel().UpdateMember(m1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return *o1, *m1
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestCase struct {
|
||||||
|
name string
|
||||||
|
pushProp string
|
||||||
|
userNotifyProp string
|
||||||
|
isDirect bool
|
||||||
|
withUnreads bool
|
||||||
|
withMentions bool
|
||||||
|
}
|
||||||
|
ttcc := []TestCase{}
|
||||||
|
|
||||||
|
channelNotifyProps := []string{model.ChannelNotifyDefault, model.ChannelNotifyAll, model.ChannelNotifyMention, model.ChannelNotifyNone}
|
||||||
|
userNotifyProps := []string{model.UserNotifyAll, model.UserNotifyMention, model.UserNotifyHere, model.UserNotifyNone}
|
||||||
|
boolRange := []bool{true, false}
|
||||||
|
|
||||||
|
nameTemplate := "pushProp: %s, userPushProp: %s, direct: %t, unreads: %t, mentions: %t"
|
||||||
|
for _, pushProp := range channelNotifyProps {
|
||||||
|
for _, userNotifyProp := range userNotifyProps {
|
||||||
|
for _, isDirect := range boolRange {
|
||||||
|
for _, withUnreads := range boolRange {
|
||||||
|
ttcc = append(ttcc, TestCase{
|
||||||
|
name: fmt.Sprintf(nameTemplate, pushProp, userNotifyProp, isDirect, withUnreads, false),
|
||||||
|
pushProp: pushProp,
|
||||||
|
userNotifyProp: userNotifyProp,
|
||||||
|
isDirect: isDirect,
|
||||||
|
withUnreads: withUnreads,
|
||||||
|
withMentions: false,
|
||||||
|
})
|
||||||
|
if withUnreads {
|
||||||
|
ttcc = append(ttcc, TestCase{
|
||||||
|
name: fmt.Sprintf(nameTemplate, pushProp, userNotifyProp, isDirect, withUnreads, true),
|
||||||
|
pushProp: pushProp,
|
||||||
|
userNotifyProp: userNotifyProp,
|
||||||
|
isDirect: isDirect,
|
||||||
|
withUnreads: withUnreads,
|
||||||
|
withMentions: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range ttcc {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
o1, m1 := setupMembership(tc.pushProp, tc.withUnreads, tc.withMentions, tc.isDirect, model.NewId())
|
||||||
|
userNotifyProps := model.GetDefaultChannelNotifyProps()
|
||||||
|
userNotifyProps[model.PushNotifyProp] = tc.userNotifyProp
|
||||||
|
unreads, mentions, times, err := ss.Channel().GetChannelsWithUnreadsAndWithMentions(context.Background(), []string{o1.Id}, m1.UserId, userNotifyProps)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedUnreadsLength := 0
|
||||||
|
if tc.withUnreads {
|
||||||
|
expectedUnreadsLength = 1
|
||||||
|
}
|
||||||
|
require.Len(t, unreads, expectedUnreadsLength)
|
||||||
|
|
||||||
|
propToUse := tc.pushProp
|
||||||
|
if tc.pushProp == model.ChannelNotifyDefault {
|
||||||
|
propToUse = tc.userNotifyProp
|
||||||
|
}
|
||||||
|
expectedMentionsLength := 0
|
||||||
|
if (tc.isDirect && tc.withUnreads) || (propToUse == model.UserNotifyAll && tc.withUnreads) || (propToUse == model.UserNotifyMention && tc.withMentions) {
|
||||||
|
expectedMentionsLength = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Len(t, mentions, expectedMentionsLength)
|
||||||
|
require.Equal(t, o1.LastPostAt, times[o1.Id])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("multiple channels", func(t *testing.T) {
|
||||||
|
userId := model.NewId()
|
||||||
|
o1, _ := setupMembership(model.ChannelNotifyDefault, true, true, false, userId)
|
||||||
|
o2, _ := setupMembership(model.ChannelNotifyDefault, true, true, false, userId)
|
||||||
|
|
||||||
|
userNotifyProps := model.GetDefaultChannelNotifyProps()
|
||||||
|
userNotifyProps[model.PushNotifyProp] = model.UserNotifyMention
|
||||||
|
|
||||||
|
unreads, mentions, times, err := ss.Channel().GetChannelsWithUnreadsAndWithMentions(context.Background(), []string{o1.Id, o2.Id}, userId, userNotifyProps)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Contains(t, unreads, o1.Id)
|
||||||
|
require.Contains(t, unreads, o2.Id)
|
||||||
|
require.Contains(t, mentions, o1.Id)
|
||||||
|
require.Contains(t, mentions, o2.Id)
|
||||||
|
require.Equal(t, o1.LastPostAt, times[o1.Id])
|
||||||
|
require.Equal(t, o2.LastPostAt, times[o2.Id])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non existing channel", func(t *testing.T) {
|
||||||
|
userNotifyProps := model.GetDefaultChannelNotifyProps()
|
||||||
|
userNotifyProps[model.PushNotifyProp] = model.UserNotifyMention
|
||||||
|
unreads, mentions, times, err := ss.Channel().GetChannelsWithUnreadsAndWithMentions(context.Background(), []string{"foo"}, "foo", userNotifyProps)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, unreads, 0)
|
||||||
|
require.Len(t, mentions, 0)
|
||||||
|
require.Len(t, times, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -986,6 +986,50 @@ func (_m *ChannelStore) GetChannelsWithTeamDataByIds(channelIds []string, includ
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetChannelsWithUnreadsAndWithMentions provides a mock function with given fields: ctx, channelIDs, userID, userNotifyProps
|
||||||
|
func (_m *ChannelStore) GetChannelsWithUnreadsAndWithMentions(ctx context.Context, channelIDs []string, userID string, userNotifyProps model.StringMap) ([]string, []string, map[string]int64, error) {
|
||||||
|
ret := _m.Called(ctx, channelIDs, userID, userNotifyProps)
|
||||||
|
|
||||||
|
var r0 []string
|
||||||
|
var r1 []string
|
||||||
|
var r2 map[string]int64
|
||||||
|
var r3 error
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, []string, string, model.StringMap) ([]string, []string, map[string]int64, error)); ok {
|
||||||
|
return rf(ctx, channelIDs, userID, userNotifyProps)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, []string, string, model.StringMap) []string); ok {
|
||||||
|
r0 = rf(ctx, channelIDs, userID, userNotifyProps)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, []string, string, model.StringMap) []string); ok {
|
||||||
|
r1 = rf(ctx, channelIDs, userID, userNotifyProps)
|
||||||
|
} else {
|
||||||
|
if ret.Get(1) != nil {
|
||||||
|
r1 = ret.Get(1).([]string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(2).(func(context.Context, []string, string, model.StringMap) map[string]int64); ok {
|
||||||
|
r2 = rf(ctx, channelIDs, userID, userNotifyProps)
|
||||||
|
} else {
|
||||||
|
if ret.Get(2) != nil {
|
||||||
|
r2 = ret.Get(2).(map[string]int64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(3).(func(context.Context, []string, string, model.StringMap) error); ok {
|
||||||
|
r3 = rf(ctx, channelIDs, userID, userNotifyProps)
|
||||||
|
} else {
|
||||||
|
r3 = ret.Error(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1, r2, r3
|
||||||
|
}
|
||||||
|
|
||||||
// GetDeleted provides a mock function with given fields: team_id, offset, limit, userID
|
// GetDeleted provides a mock function with given fields: team_id, offset, limit, userID
|
||||||
func (_m *ChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) {
|
func (_m *ChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) {
|
||||||
ret := _m.Called(team_id, offset, limit, userID)
|
ret := _m.Called(team_id, offset, limit, userID)
|
||||||
|
|||||||
@@ -1232,6 +1232,22 @@ func (s *TimerLayerChannelStore) GetChannelsWithTeamDataByIds(channelIds []strin
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TimerLayerChannelStore) GetChannelsWithUnreadsAndWithMentions(ctx context.Context, channelIDs []string, userID string, userNotifyProps model.StringMap) ([]string, []string, map[string]int64, error) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
result, resultVar1, resultVar2, err := s.ChannelStore.GetChannelsWithUnreadsAndWithMentions(ctx, channelIDs, userID, userNotifyProps)
|
||||||
|
|
||||||
|
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||||
|
if s.Root.Metrics != nil {
|
||||||
|
success := "false"
|
||||||
|
if err == nil {
|
||||||
|
success = "true"
|
||||||
|
}
|
||||||
|
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelsWithUnreadsAndWithMentions", success, elapsed)
|
||||||
|
}
|
||||||
|
return result, resultVar1, resultVar2, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *TimerLayerChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) {
|
func (s *TimerLayerChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
|||||||
@@ -4783,6 +4783,10 @@
|
|||||||
"id": "app.channel.get_channels_member_count.find.app_error",
|
"id": "app.channel.get_channels_member_count.find.app_error",
|
||||||
"translation": "Unable to find member count."
|
"translation": "Unable to find member count."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "app.channel.get_channels_with_unreads_and_with_mentions.app_error",
|
||||||
|
"translation": "Unable to check unreads and mentions"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "app.channel.get_deleted.existing.app_error",
|
"id": "app.channel.get_deleted.existing.app_error",
|
||||||
"translation": "Unable to find the existing deleted channel."
|
"translation": "Unable to find the existing deleted channel."
|
||||||
@@ -6743,6 +6747,10 @@
|
|||||||
"id": "app.terms_of_service.get.no_rows.app_error",
|
"id": "app.terms_of_service.get.no_rows.app_error",
|
||||||
"translation": "No terms of service found."
|
"translation": "No terms of service found."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "app.thread.mark_all_as_read_by_channels.app_error",
|
||||||
|
"translation": "Unable to mark all threads as read by channel"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "app.update_error",
|
"id": "app.update_error",
|
||||||
"translation": "update error"
|
"translation": "update error"
|
||||||
|
|||||||
@@ -3540,6 +3540,27 @@ func (c *Client4) ViewChannel(ctx context.Context, userId string, view *ChannelV
|
|||||||
return ch, BuildResponse(r), nil
|
return ch, BuildResponse(r), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadMultipleChannels performs a view action on several channels at the same time for a user.
|
||||||
|
func (c *Client4) ReadMultipleChannels(ctx context.Context, userId string, channelIds []string) (*ChannelViewResponse, *Response, error) {
|
||||||
|
url := fmt.Sprintf(c.channelsRoute()+"/members/%v/mark_read", userId)
|
||||||
|
buf, err := json.Marshal(channelIds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, NewAppError("ReadMultipleChannels", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
r, err := c.DoAPIPostBytes(ctx, url, buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildResponse(r), err
|
||||||
|
}
|
||||||
|
defer closeBody(r)
|
||||||
|
|
||||||
|
var ch *ChannelViewResponse
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&ch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildResponse(r), NewAppError("ReadMultipleChannels", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
return ch, BuildResponse(r), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetChannelUnread will return a ChannelUnread object that contains the number of
|
// GetChannelUnread will return a ChannelUnread object that contains the number of
|
||||||
// unread messages and mentions for a user.
|
// unread messages and mentions for a user.
|
||||||
func (c *Client4) GetChannelUnread(ctx context.Context, channelId, userId string) (*ChannelUnread, *Response, error) {
|
func (c *Client4) GetChannelUnread(ctx context.Context, channelId, userId string) (*ChannelUnread, *Response, error) {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ const (
|
|||||||
WebsocketEventResponse = "response"
|
WebsocketEventResponse = "response"
|
||||||
WebsocketEventEmojiAdded = "emoji_added"
|
WebsocketEventEmojiAdded = "emoji_added"
|
||||||
WebsocketEventChannelViewed = "channel_viewed"
|
WebsocketEventChannelViewed = "channel_viewed"
|
||||||
|
WebsocketEventMultipleChannelsViewed = "multiple_channels_viewed"
|
||||||
WebsocketEventPluginStatusesChanged = "plugin_statuses_changed"
|
WebsocketEventPluginStatusesChanged = "plugin_statuses_changed"
|
||||||
WebsocketEventPluginEnabled = "plugin_enabled"
|
WebsocketEventPluginEnabled = "plugin_enabled"
|
||||||
WebsocketEventPluginDisabled = "plugin_disabled"
|
WebsocketEventPluginDisabled = "plugin_disabled"
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ describe('actions/new_post', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
|
type: ChannelTypes.RECEIVED_LAST_VIEWED_AT,
|
||||||
data: {
|
data: {
|
||||||
channel_id: channelId,
|
channel_id: channelId,
|
||||||
},
|
},
|
||||||
@@ -377,7 +377,7 @@ describe('actions/new_post', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
|
type: ChannelTypes.RECEIVED_LAST_VIEWED_AT,
|
||||||
data: {
|
data: {
|
||||||
channel_id: channelId,
|
channel_id: channelId,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/pre
|
|||||||
import {
|
import {
|
||||||
actionsToMarkChannelAsRead,
|
actionsToMarkChannelAsRead,
|
||||||
actionsToMarkChannelAsUnread,
|
actionsToMarkChannelAsUnread,
|
||||||
actionsToMarkChannelAsViewed,
|
|
||||||
markChannelAsViewedOnServer,
|
markChannelAsViewedOnServer,
|
||||||
} from 'mattermost-redux/actions/channels';
|
} from 'mattermost-redux/actions/channels';
|
||||||
import * as PostActions from 'mattermost-redux/actions/posts';
|
import * as PostActions from 'mattermost-redux/actions/posts';
|
||||||
@@ -123,10 +122,7 @@ export function setChannelReadAndViewed(dispatch: DispatchFunc, getState: GetSta
|
|||||||
dispatch(markChannelAsViewedOnServer(post.channel_id));
|
dispatch(markChannelAsViewedOnServer(post.channel_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return actionsToMarkChannelAsRead(getState, post.channel_id);
|
||||||
...actionsToMarkChannelAsRead(getState, post.channel_id),
|
|
||||||
...actionsToMarkChannelAsViewed(getState, post.channel_id),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return actionsToMarkChannelAsUnread(getState, websocketMessageProps.team_id, post.channel_id, websocketMessageProps.mentions, fetchedChannelMember, post.root_id === '', post?.metadata?.priority?.priority);
|
return actionsToMarkChannelAsUnread(getState, websocketMessageProps.team_id, post.channel_id, websocketMessageProps.mentions, fetchedChannelMember, post.root_id === '', post?.metadata?.priority?.priority);
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ import {
|
|||||||
getChannelAndMyMember,
|
getChannelAndMyMember,
|
||||||
getMyChannelMember,
|
getMyChannelMember,
|
||||||
getChannelStats,
|
getChannelStats,
|
||||||
viewChannel,
|
markMultipleChannelsAsRead,
|
||||||
markChannelAsRead,
|
|
||||||
getChannelMemberCountsByGroup,
|
getChannelMemberCountsByGroup,
|
||||||
} from 'mattermost-redux/actions/channels';
|
} from 'mattermost-redux/actions/channels';
|
||||||
import {getCloudSubscription} from 'mattermost-redux/actions/cloud';
|
import {getCloudSubscription} from 'mattermost-redux/actions/cloud';
|
||||||
@@ -456,8 +455,8 @@ export function handleEvent(msg) {
|
|||||||
handleAddEmoji(msg);
|
handleAddEmoji(msg);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SocketEvents.CHANNEL_VIEWED:
|
case SocketEvents.MULTIPLE_CHANNELS_VIEWED:
|
||||||
handleChannelViewedEvent(msg);
|
handleMultipleChannelsViewedEvent(msg);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SocketEvents.PLUGIN_ENABLED:
|
case SocketEvents.PLUGIN_ENABLED:
|
||||||
@@ -741,15 +740,6 @@ export function handlePostEditEvent(msg) {
|
|||||||
dispatch(receivedPost(post, crtEnabled));
|
dispatch(receivedPost(post, crtEnabled));
|
||||||
|
|
||||||
getProfilesAndStatusesForPosts([post], dispatch, getState);
|
getProfilesAndStatusesForPosts([post], dispatch, getState);
|
||||||
const currentChannelId = getCurrentChannelId(getState());
|
|
||||||
|
|
||||||
// Update channel state
|
|
||||||
if (currentChannelId === msg.broadcast.channel_id) {
|
|
||||||
dispatch(getChannelStats(currentChannelId));
|
|
||||||
if (window.isActive) {
|
|
||||||
dispatch(viewChannel(currentChannelId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePostDeleteEvent(msg) {
|
async function handlePostDeleteEvent(msg) {
|
||||||
@@ -1291,11 +1281,9 @@ function handleReactionRemovedEvent(msg) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChannelViewedEvent(msg) {
|
function handleMultipleChannelsViewedEvent(msg) {
|
||||||
// Useful for when multiple devices have the app open to different channels
|
if (getCurrentUserId(getState()) === msg.broadcast.user_id) {
|
||||||
if ((!window.isActive || getCurrentChannelId(getState()) !== msg.data.channel_id) &&
|
dispatch(markMultipleChannelsAsRead(msg.data.channel_times));
|
||||||
getCurrentUserId(getState()) === msg.broadcast.user_id) {
|
|
||||||
dispatch(markChannelAsRead(msg.data.channel_id, '', false));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import {Channel} from '@mattermost/types/channels';
|
|||||||
import {DispatchFunc, GenericAction} from 'mattermost-redux/types/actions';
|
import {DispatchFunc, GenericAction} from 'mattermost-redux/types/actions';
|
||||||
|
|
||||||
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
|
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
|
||||||
import {viewChannel} from 'mattermost-redux/actions/channels';
|
import {markChannelAsViewedOnServer, updateApproximateViewTime} from 'mattermost-redux/actions/channels';
|
||||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
import {getCurrentChannelId, isManuallyUnread} from 'mattermost-redux/selectors/entities/channels';
|
||||||
import {getLicense, getConfig} from 'mattermost-redux/selectors/entities/general';
|
import {getLicense, getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {getCurrentUser, shouldShowTermsOfService} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUser, shouldShowTermsOfService} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
@@ -34,10 +34,12 @@ function mapStateToProps(state: GlobalState, ownProps: Props) {
|
|||||||
const license = getLicense(state);
|
const license = getLicense(state);
|
||||||
const config = getConfig(state);
|
const config = getConfig(state);
|
||||||
const showTermsOfService = shouldShowTermsOfService(state);
|
const showTermsOfService = shouldShowTermsOfService(state);
|
||||||
|
const currentChannelId = getCurrentChannelId(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentUser: getCurrentUser(state),
|
currentUser: getCurrentUser(state),
|
||||||
currentChannelId: getCurrentChannelId(state),
|
currentChannelId,
|
||||||
|
isCurrentChannelManuallyUnread: isManuallyUnread(state, currentChannelId),
|
||||||
mfaRequired: checkIfMFARequired(getCurrentUser(state), license, config, ownProps.match.url),
|
mfaRequired: checkIfMFARequired(getCurrentUser(state), license, config, ownProps.match.url),
|
||||||
enableTimezone: config.ExperimentalTimezone === 'true',
|
enableTimezone: config.ExperimentalTimezone === 'true',
|
||||||
showTermsOfService,
|
showTermsOfService,
|
||||||
@@ -60,7 +62,8 @@ function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
|||||||
actions: bindActionCreators({
|
actions: bindActionCreators({
|
||||||
autoUpdateTimezone,
|
autoUpdateTimezone,
|
||||||
getChannelURLAction,
|
getChannelURLAction,
|
||||||
viewChannel,
|
markChannelAsViewedOnServer,
|
||||||
|
updateApproximateViewTime,
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ describe('components/logged_in/LoggedIn', () => {
|
|||||||
actions: {
|
actions: {
|
||||||
autoUpdateTimezone: jest.fn(),
|
autoUpdateTimezone: jest.fn(),
|
||||||
getChannelURLAction: jest.fn(),
|
getChannelURLAction: jest.fn(),
|
||||||
viewChannel: jest.fn(),
|
markChannelAsViewedOnServer: jest.fn(),
|
||||||
|
updateApproximateViewTime: jest.fn(),
|
||||||
},
|
},
|
||||||
|
isCurrentChannelManuallyUnread: false,
|
||||||
showTermsOfService: false,
|
showTermsOfService: false,
|
||||||
location: {
|
location: {
|
||||||
pathname: '/',
|
pathname: '/',
|
||||||
|
|||||||
@@ -29,13 +29,15 @@ declare global {
|
|||||||
export type Props = {
|
export type Props = {
|
||||||
currentUser?: UserProfile;
|
currentUser?: UserProfile;
|
||||||
currentChannelId?: string;
|
currentChannelId?: string;
|
||||||
|
isCurrentChannelManuallyUnread: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
mfaRequired: boolean;
|
mfaRequired: boolean;
|
||||||
enableTimezone: boolean;
|
enableTimezone: boolean;
|
||||||
actions: {
|
actions: {
|
||||||
autoUpdateTimezone: (deviceTimezone: string) => void;
|
autoUpdateTimezone: (deviceTimezone: string) => void;
|
||||||
getChannelURLAction: (channel: Channel, teamId: string, url: string) => void;
|
getChannelURLAction: (channel: Channel, teamId: string, url: string) => void;
|
||||||
viewChannel: (channelId: string, prevChannelId?: string) => void;
|
markChannelAsViewedOnServer: (channelId: string) => void;
|
||||||
|
updateApproximateViewTime: (channelId: string) => void;
|
||||||
};
|
};
|
||||||
showTermsOfService: boolean;
|
showTermsOfService: boolean;
|
||||||
location: {
|
location: {
|
||||||
@@ -226,8 +228,9 @@ export default class LoggedIn extends React.PureComponent<Props> {
|
|||||||
private handleBeforeUnload = (): void => {
|
private handleBeforeUnload = (): void => {
|
||||||
// remove the event listener to prevent getting stuck in a loop
|
// remove the event listener to prevent getting stuck in a loop
|
||||||
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
||||||
if (document.cookie.indexOf('MMUSERID=') > -1) {
|
if (document.cookie.indexOf('MMUSERID=') > -1 && this.props.currentChannelId && !this.props.isCurrentChannelManuallyUnread) {
|
||||||
this.props.actions.viewChannel('', this.props.currentChannelId || '');
|
this.props.actions.updateApproximateViewTime(this.props.currentChannelId);
|
||||||
|
this.props.actions.markChannelAsViewedOnServer(this.props.currentChannelId);
|
||||||
}
|
}
|
||||||
WebSocketActions.close();
|
WebSocketActions.close();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {Dispatch, bindActionCreators, ActionCreatorsMapObject} from 'redux';
|
|||||||
import {getRecentPostsChunkInChannel, makeGetPostsChunkAroundPost, getUnreadPostsChunk, getPost, isPostsChunkIncludingUnreadsPosts, getLimitedViews} from 'mattermost-redux/selectors/entities/posts';
|
import {getRecentPostsChunkInChannel, makeGetPostsChunkAroundPost, getUnreadPostsChunk, getPost, isPostsChunkIncludingUnreadsPosts, getLimitedViews} from 'mattermost-redux/selectors/entities/posts';
|
||||||
import {memoizeResult} from 'mattermost-redux/utils/helpers';
|
import {memoizeResult} from 'mattermost-redux/utils/helpers';
|
||||||
import {Action} from 'mattermost-redux/types/actions';
|
import {Action} from 'mattermost-redux/types/actions';
|
||||||
import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
|
import {markChannelAsRead} from 'mattermost-redux/actions/channels';
|
||||||
import {makePreparePostIdsForPostList} from 'mattermost-redux/utils/post_list';
|
import {makePreparePostIdsForPostList} from 'mattermost-redux/utils/post_list';
|
||||||
import {RequestStatus} from 'mattermost-redux/constants';
|
import {RequestStatus} from 'mattermost-redux/constants';
|
||||||
|
|
||||||
@@ -111,7 +111,6 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
|||||||
loadPostsAround,
|
loadPostsAround,
|
||||||
checkAndSetMobileView,
|
checkAndSetMobileView,
|
||||||
syncPostsInChannel,
|
syncPostsInChannel,
|
||||||
markChannelAsViewed,
|
|
||||||
markChannelAsRead,
|
markChannelAsRead,
|
||||||
updateNewMessagesAtInChannel,
|
updateNewMessagesAtInChannel,
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ const actionsProp = {
|
|||||||
syncPostsInChannel: jest.fn().mockResolvedValue({}),
|
syncPostsInChannel: jest.fn().mockResolvedValue({}),
|
||||||
loadLatestPosts: jest.fn().mockImplementation(() => Promise.resolve({atLatestMessage: true, atOldestmessage: true})),
|
loadLatestPosts: jest.fn().mockImplementation(() => Promise.resolve({atLatestMessage: true, atOldestmessage: true})),
|
||||||
checkAndSetMobileView: jest.fn(),
|
checkAndSetMobileView: jest.fn(),
|
||||||
markChannelAsViewed: jest.fn(),
|
|
||||||
markChannelAsRead: jest.fn(),
|
markChannelAsRead: jest.fn(),
|
||||||
updateNewMessagesAtInChannel: jest.fn(),
|
updateNewMessagesAtInChannel: jest.fn(),
|
||||||
toggleShouldStartFromBottomWhenUnread: jest.fn(),
|
toggleShouldStartFromBottomWhenUnread: jest.fn(),
|
||||||
@@ -250,7 +249,6 @@ describe('components/post_view/post_list', () => {
|
|||||||
|
|
||||||
await wrapper.instance().postsOnLoad('undefined');
|
await wrapper.instance().postsOnLoad('undefined');
|
||||||
expect(actionsProp.markChannelAsRead).toHaveBeenCalledWith(baseProps.channelId);
|
expect(actionsProp.markChannelAsRead).toHaveBeenCalledWith(baseProps.channelId);
|
||||||
expect(actionsProp.markChannelAsViewed).toHaveBeenCalledWith(baseProps.channelId);
|
|
||||||
});
|
});
|
||||||
test('Should not call markChannelAsReadAndViewed as it is a permalink', async () => {
|
test('Should not call markChannelAsReadAndViewed as it is a permalink', async () => {
|
||||||
const emptyPostList: string[] = [];
|
const emptyPostList: string[] = [];
|
||||||
@@ -261,7 +259,6 @@ describe('components/post_view/post_list', () => {
|
|||||||
|
|
||||||
await actionsProp.loadPostsAround();
|
await actionsProp.loadPostsAround();
|
||||||
expect(actionsProp.markChannelAsRead).not.toHaveBeenCalled();
|
expect(actionsProp.markChannelAsRead).not.toHaveBeenCalled();
|
||||||
expect(actionsProp.markChannelAsViewed).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -141,8 +141,6 @@ export interface Props {
|
|||||||
*/
|
*/
|
||||||
loadLatestPosts: (channelId: string) => Promise<void>;
|
loadLatestPosts: (channelId: string) => Promise<void>;
|
||||||
|
|
||||||
markChannelAsViewed: (channelId: string) => void;
|
|
||||||
|
|
||||||
markChannelAsRead: (channelId: string) => void;
|
markChannelAsRead: (channelId: string) => void;
|
||||||
updateNewMessagesAtInChannel: typeof updateNewMessagesAtInChannel;
|
updateNewMessagesAtInChannel: typeof updateNewMessagesAtInChannel;
|
||||||
};
|
};
|
||||||
@@ -230,7 +228,9 @@ export default class PostList extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!focusedPostId) {
|
if (!focusedPostId) {
|
||||||
this.markChannelAsReadAndViewed(channelId);
|
// Posts are marked as read from here to not cause a race when loading posts
|
||||||
|
// marking channel as read and viewed after calling for posts in channel
|
||||||
|
this.props.actions.markChannelAsRead(channelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.mounted) {
|
if (this.mounted) {
|
||||||
@@ -274,13 +274,6 @@ export default class PostList extends React.PureComponent<Props, State> {
|
|||||||
return {error};
|
return {error};
|
||||||
};
|
};
|
||||||
|
|
||||||
markChannelAsReadAndViewed = (channelId: string) => {
|
|
||||||
// Posts are marked as read from here to not cause a race when loading posts
|
|
||||||
// marking channel as read and viewed after calling for posts in channel
|
|
||||||
this.props.actions.markChannelAsViewed(channelId);
|
|
||||||
this.props.actions.markChannelAsRead(channelId);
|
|
||||||
};
|
|
||||||
|
|
||||||
getOldestVisiblePostId = () => {
|
getOldestVisiblePostId = () => {
|
||||||
return getOldestPostId(this.props.postListIds || []);
|
return getOldestPostId(this.props.postListIds || []);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,18 +27,7 @@ exports[`components/sidebar should match snapshot 1`] = `
|
|||||||
id="lhsNavigator"
|
id="lhsNavigator"
|
||||||
role="application"
|
role="application"
|
||||||
>
|
>
|
||||||
<Connect(ChannelNavigator)
|
<Connect(ChannelNavigator) />
|
||||||
canCreateChannel={true}
|
|
||||||
canJoinPublicChannel={true}
|
|
||||||
handleOpenDirectMessagesModal={[Function]}
|
|
||||||
invitePeopleModal={[Function]}
|
|
||||||
showCreateCategoryModal={[Function]}
|
|
||||||
showCreateUserGroupModal={[Function]}
|
|
||||||
showMoreChannelsModal={[Function]}
|
|
||||||
showNewChannelModal={[Function]}
|
|
||||||
unreadFilterEnabled={false}
|
|
||||||
userGroupsEnabled={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="sidebar--left__icons"
|
className="sidebar--left__icons"
|
||||||
@@ -81,18 +70,7 @@ exports[`components/sidebar should match snapshot when direct channels modal is
|
|||||||
id="lhsNavigator"
|
id="lhsNavigator"
|
||||||
role="application"
|
role="application"
|
||||||
>
|
>
|
||||||
<Connect(ChannelNavigator)
|
<Connect(ChannelNavigator) />
|
||||||
canCreateChannel={true}
|
|
||||||
canJoinPublicChannel={true}
|
|
||||||
handleOpenDirectMessagesModal={[Function]}
|
|
||||||
invitePeopleModal={[Function]}
|
|
||||||
showCreateCategoryModal={[Function]}
|
|
||||||
showCreateUserGroupModal={[Function]}
|
|
||||||
showMoreChannelsModal={[Function]}
|
|
||||||
showNewChannelModal={[Function]}
|
|
||||||
unreadFilterEnabled={false}
|
|
||||||
userGroupsEnabled={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="sidebar--left__icons"
|
className="sidebar--left__icons"
|
||||||
@@ -139,18 +117,7 @@ exports[`components/sidebar should match snapshot when more channels modal is op
|
|||||||
id="lhsNavigator"
|
id="lhsNavigator"
|
||||||
role="application"
|
role="application"
|
||||||
>
|
>
|
||||||
<Connect(ChannelNavigator)
|
<Connect(ChannelNavigator) />
|
||||||
canCreateChannel={true}
|
|
||||||
canJoinPublicChannel={true}
|
|
||||||
handleOpenDirectMessagesModal={[Function]}
|
|
||||||
invitePeopleModal={[Function]}
|
|
||||||
showCreateCategoryModal={[Function]}
|
|
||||||
showCreateUserGroupModal={[Function]}
|
|
||||||
showMoreChannelsModal={[Function]}
|
|
||||||
showNewChannelModal={[Function]}
|
|
||||||
unreadFilterEnabled={false}
|
|
||||||
userGroupsEnabled={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="sidebar--left__icons"
|
className="sidebar--left__icons"
|
||||||
|
|||||||
@@ -13,26 +13,11 @@ let props: Props;
|
|||||||
describe('Components/ChannelNavigator', () => {
|
describe('Components/ChannelNavigator', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
props = {
|
props = {
|
||||||
canGoForward: true,
|
|
||||||
canGoBack: true,
|
|
||||||
canJoinPublicChannel: true,
|
|
||||||
showMoreChannelsModal: jest.fn(),
|
|
||||||
showCreateUserGroupModal: jest.fn(),
|
|
||||||
invitePeopleModal: jest.fn(),
|
|
||||||
showNewChannelModal: jest.fn(),
|
|
||||||
showCreateCategoryModal: jest.fn(),
|
|
||||||
handleOpenDirectMessagesModal: jest.fn(),
|
|
||||||
unreadFilterEnabled: true,
|
|
||||||
canCreateChannel: true,
|
|
||||||
showUnreadsCategory: true,
|
showUnreadsCategory: true,
|
||||||
isQuickSwitcherOpen: false,
|
isQuickSwitcherOpen: false,
|
||||||
userGroupsEnabled: false,
|
|
||||||
canCreateCustomGroups: true,
|
|
||||||
actions: {
|
actions: {
|
||||||
openModal: jest.fn(),
|
openModal: jest.fn(),
|
||||||
closeModal: jest.fn(),
|
closeModal: jest.fn(),
|
||||||
goBack: jest.fn(),
|
|
||||||
goForward: jest.fn(),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,26 +18,11 @@ import * as Utils from 'utils/utils';
|
|||||||
import ChannelFilter from '../channel_filter';
|
import ChannelFilter from '../channel_filter';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
canGoForward: boolean;
|
|
||||||
canGoBack: boolean;
|
|
||||||
canJoinPublicChannel: boolean;
|
|
||||||
showMoreChannelsModal: () => void;
|
|
||||||
showCreateUserGroupModal: () => void;
|
|
||||||
invitePeopleModal: () => void;
|
|
||||||
showNewChannelModal: () => void;
|
|
||||||
showCreateCategoryModal: () => void;
|
|
||||||
handleOpenDirectMessagesModal: (e: Event) => void;
|
|
||||||
unreadFilterEnabled: boolean;
|
|
||||||
canCreateChannel: boolean;
|
|
||||||
showUnreadsCategory: boolean;
|
showUnreadsCategory: boolean;
|
||||||
isQuickSwitcherOpen: boolean;
|
isQuickSwitcherOpen: boolean;
|
||||||
userGroupsEnabled: boolean;
|
|
||||||
canCreateCustomGroups: boolean;
|
|
||||||
actions: {
|
actions: {
|
||||||
openModal: <P>(modalData: ModalData<P>) => void;
|
openModal: <P>(modalData: ModalData<P>) => void;
|
||||||
closeModal: (modalId: string) => void;
|
closeModal: (modalId: string) => void;
|
||||||
goBack: () => void;
|
|
||||||
goForward: () => void;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -100,16 +85,6 @@ export default class ChannelNavigator extends React.PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
goBack = () => {
|
|
||||||
trackEvent('ui', 'ui_history_back');
|
|
||||||
this.props.actions.goBack();
|
|
||||||
};
|
|
||||||
|
|
||||||
goForward = () => {
|
|
||||||
trackEvent('ui', 'ui_history_forward');
|
|
||||||
this.props.actions.goForward();
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className={'SidebarChannelNavigator webapp'}>
|
<div className={'SidebarChannelNavigator webapp'}>
|
||||||
|
|||||||
@@ -5,12 +5,9 @@ import {connect} from 'react-redux';
|
|||||||
import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
|
import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
|
||||||
|
|
||||||
import {Action} from 'mattermost-redux/types/actions';
|
import {Action} from 'mattermost-redux/types/actions';
|
||||||
import {shouldShowUnreadsCategory, isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
import {shouldShowUnreadsCategory} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {haveISystemPermission} from 'mattermost-redux/selectors/entities/roles';
|
|
||||||
import Permissions from 'mattermost-redux/constants/permissions';
|
|
||||||
|
|
||||||
import {openModal, closeModal} from 'actions/views/modals';
|
import {openModal, closeModal} from 'actions/views/modals';
|
||||||
import {getHistory} from 'utils/browser_history';
|
|
||||||
import {ModalIdentifiers} from 'utils/constants';
|
import {ModalIdentifiers} from 'utils/constants';
|
||||||
import {isModalOpen} from 'selectors/views/modals';
|
import {isModalOpen} from 'selectors/views/modals';
|
||||||
|
|
||||||
@@ -19,37 +16,16 @@ import {GlobalState} from 'types/store';
|
|||||||
|
|
||||||
import ChannelNavigator from './channel_navigator';
|
import ChannelNavigator from './channel_navigator';
|
||||||
|
|
||||||
// TODO: For Phase 1. Will revisit history in Phase 2
|
|
||||||
function goBack() {
|
|
||||||
return () => {
|
|
||||||
getHistory().goBack();
|
|
||||||
return {data: null};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function goForward() {
|
|
||||||
return () => {
|
|
||||||
getHistory().goForward();
|
|
||||||
return {data: null};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapStateToProps(state: GlobalState) {
|
function mapStateToProps(state: GlobalState) {
|
||||||
const canCreateCustomGroups = haveISystemPermission(state, {permission: Permissions.CREATE_CUSTOM_GROUP}) && isCustomGroupsEnabled(state);
|
|
||||||
return {
|
return {
|
||||||
canGoBack: true, // TODO: Phase 1 only
|
|
||||||
canGoForward: true,
|
|
||||||
showUnreadsCategory: shouldShowUnreadsCategory(state),
|
showUnreadsCategory: shouldShowUnreadsCategory(state),
|
||||||
isQuickSwitcherOpen: isModalOpen(state, ModalIdentifiers.QUICK_SWITCH),
|
isQuickSwitcherOpen: isModalOpen(state, ModalIdentifiers.QUICK_SWITCH),
|
||||||
canCreateCustomGroups,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type Actions = {
|
type Actions = {
|
||||||
openModal: <P>(modalData: ModalData<P>) => void;
|
openModal: <P>(modalData: ModalData<P>) => void;
|
||||||
closeModal: (modalId: string) => void;
|
closeModal: (modalId: string) => void;
|
||||||
goBack: () => void;
|
|
||||||
goForward: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: Dispatch) {
|
function mapDispatchToProps(dispatch: Dispatch) {
|
||||||
@@ -57,8 +33,6 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
|||||||
actions: bindActionCreators<ActionCreatorsMapObject<Action>, Actions>({
|
actions: bindActionCreators<ActionCreatorsMapObject<Action>, Actions>({
|
||||||
openModal,
|
openModal,
|
||||||
closeModal,
|
closeModal,
|
||||||
goBack,
|
|
||||||
goForward,
|
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ import {connect} from 'react-redux';
|
|||||||
import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
|
import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
|
||||||
|
|
||||||
import {fetchMyCategories} from 'mattermost-redux/actions/channel_categories';
|
import {fetchMyCategories} from 'mattermost-redux/actions/channel_categories';
|
||||||
import {Preferences} from 'mattermost-redux/constants';
|
|
||||||
import Permissions from 'mattermost-redux/constants/permissions';
|
import Permissions from 'mattermost-redux/constants/permissions';
|
||||||
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
import {isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
import {getBool, isCustomGroupsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
|
||||||
import {haveICurrentChannelPermission, haveISystemPermission} from 'mattermost-redux/selectors/entities/roles';
|
import {haveICurrentChannelPermission, haveISystemPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||||
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {GenericAction} from 'mattermost-redux/types/actions';
|
import {GenericAction} from 'mattermost-redux/types/actions';
|
||||||
import {createCategory, clearChannelSelection} from 'actions/views/channel_sidebar';
|
import {clearChannelSelection} from 'actions/views/channel_sidebar';
|
||||||
import {isUnreadFilterEnabled} from 'selectors/views/channel_sidebar';
|
import {isUnreadFilterEnabled} from 'selectors/views/channel_sidebar';
|
||||||
import {closeModal, openModal} from 'actions/views/modals';
|
import {closeModal, openModal} from 'actions/views/modals';
|
||||||
import {closeRightHandSide} from 'actions/views/rhs';
|
import {closeRightHandSide} from 'actions/views/rhs';
|
||||||
@@ -49,13 +47,6 @@ function mapStateToProps(state: GlobalState) {
|
|||||||
canCreatePublicChannel,
|
canCreatePublicChannel,
|
||||||
canJoinPublicChannel,
|
canJoinPublicChannel,
|
||||||
isOpen: getIsLhsOpen(state),
|
isOpen: getIsLhsOpen(state),
|
||||||
hasSeenModal: getBool(
|
|
||||||
state,
|
|
||||||
Preferences.CATEGORY_WHATS_NEW_MODAL,
|
|
||||||
Preferences.HAS_SEEN_SIDEBAR_WHATS_NEW_MODAL,
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
isCloud: getLicense(state).Cloud === 'true',
|
|
||||||
unreadFilterEnabled,
|
unreadFilterEnabled,
|
||||||
isMobileView: getIsMobileView(state),
|
isMobileView: getIsMobileView(state),
|
||||||
isKeyBoardShortcutModalOpen: isModalOpen(state, ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL),
|
isKeyBoardShortcutModalOpen: isModalOpen(state, ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL),
|
||||||
@@ -68,7 +59,6 @@ function mapStateToProps(state: GlobalState) {
|
|||||||
|
|
||||||
type Actions = {
|
type Actions = {
|
||||||
fetchMyCategories: (teamId: string) => {data: boolean};
|
fetchMyCategories: (teamId: string) => {data: boolean};
|
||||||
createCategory: (teamId: string, categoryName: string) => {data: string};
|
|
||||||
openModal: <P>(modalData: ModalData<P>) => void;
|
openModal: <P>(modalData: ModalData<P>) => void;
|
||||||
clearChannelSelection: () => void;
|
clearChannelSelection: () => void;
|
||||||
closeModal: (modalId: string) => void;
|
closeModal: (modalId: string) => void;
|
||||||
@@ -79,7 +69,6 @@ function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
|||||||
return {
|
return {
|
||||||
actions: bindActionCreators<ActionCreatorsMapObject, Actions>({
|
actions: bindActionCreators<ActionCreatorsMapObject, Actions>({
|
||||||
clearChannelSelection,
|
clearChannelSelection,
|
||||||
createCategory,
|
|
||||||
fetchMyCategories,
|
fetchMyCategories,
|
||||||
openModal,
|
openModal,
|
||||||
closeModal,
|
closeModal,
|
||||||
|
|||||||
@@ -36,16 +36,13 @@ type Props = {
|
|||||||
canCreatePrivateChannel: boolean;
|
canCreatePrivateChannel: boolean;
|
||||||
canJoinPublicChannel: boolean;
|
canJoinPublicChannel: boolean;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
hasSeenModal: boolean;
|
|
||||||
actions: {
|
actions: {
|
||||||
fetchMyCategories: (teamId: string) => {data: boolean};
|
fetchMyCategories: (teamId: string) => {data: boolean};
|
||||||
createCategory: (teamId: string, categoryName: string) => {data: string};
|
|
||||||
openModal: <P>(modalData: ModalData<P>) => void;
|
openModal: <P>(modalData: ModalData<P>) => void;
|
||||||
closeModal: (modalId: string) => void;
|
closeModal: (modalId: string) => void;
|
||||||
clearChannelSelection: () => void;
|
clearChannelSelection: () => void;
|
||||||
closeRightHandSide: () => void;
|
closeRightHandSide: () => void;
|
||||||
};
|
};
|
||||||
isCloud: boolean;
|
|
||||||
unreadFilterEnabled: boolean;
|
unreadFilterEnabled: boolean;
|
||||||
isMobileView: boolean;
|
isMobileView: boolean;
|
||||||
isKeyBoardShortcutModalOpen: boolean;
|
isKeyBoardShortcutModalOpen: boolean;
|
||||||
@@ -148,10 +145,6 @@ export default class Sidebar extends React.PureComponent<Props, State> {
|
|||||||
trackEvent('ui', 'ui_sidebar_menu_createCategory');
|
trackEvent('ui', 'ui_sidebar_menu_createCategory');
|
||||||
};
|
};
|
||||||
|
|
||||||
handleCreateCategory = (categoryName: string) => {
|
|
||||||
this.props.actions.createCategory(this.props.teamId, categoryName);
|
|
||||||
};
|
|
||||||
|
|
||||||
showMoreChannelsModal = () => {
|
showMoreChannelsModal = () => {
|
||||||
this.props.actions.openModal({
|
this.props.actions.openModal({
|
||||||
modalId: ModalIdentifiers.MORE_CHANNELS,
|
modalId: ModalIdentifiers.MORE_CHANNELS,
|
||||||
@@ -265,18 +258,7 @@ export default class Sidebar extends React.PureComponent<Props, State> {
|
|||||||
className='a11y__region'
|
className='a11y__region'
|
||||||
data-a11y-sort-order='6'
|
data-a11y-sort-order='6'
|
||||||
>
|
>
|
||||||
<ChannelNavigator
|
<ChannelNavigator/>
|
||||||
showNewChannelModal={this.showNewChannelModal}
|
|
||||||
showMoreChannelsModal={this.showMoreChannelsModal}
|
|
||||||
showCreateUserGroupModal={this.showCreateUserGroupModal}
|
|
||||||
invitePeopleModal={this.invitePeopleModal}
|
|
||||||
showCreateCategoryModal={this.showCreateCategoryModal}
|
|
||||||
canCreateChannel={this.props.canCreatePrivateChannel || this.props.canCreatePublicChannel}
|
|
||||||
canJoinPublicChannel={this.props.canJoinPublicChannel}
|
|
||||||
handleOpenDirectMessagesModal={this.handleOpenMoreDirectChannelsModal}
|
|
||||||
unreadFilterEnabled={this.props.unreadFilterEnabled}
|
|
||||||
userGroupsEnabled={this.props.userGroupsEnabled}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='sidebar--left__icons'>
|
<div className='sidebar--left__icons'>
|
||||||
<Pluggable pluggableName='LeftSidebarHeader'/>
|
<Pluggable pluggableName='LeftSidebarHeader'/>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ exports[`components/sidebar/sidebar_category should match snapshot 2`] = `
|
|||||||
muted={false}
|
muted={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<Connect(Component)
|
<Memo(SidebarCategoryMenu)
|
||||||
category={
|
category={
|
||||||
Object {
|
Object {
|
||||||
"channel_ids": Array [
|
"channel_ids": Array [
|
||||||
@@ -106,7 +106,7 @@ exports[`components/sidebar/sidebar_category should match snapshot when collapse
|
|||||||
muted={false}
|
muted={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<Connect(Component)
|
<Memo(SidebarCategoryMenu)
|
||||||
category={
|
category={
|
||||||
Object {
|
Object {
|
||||||
"channel_ids": Array [
|
"channel_ids": Array [
|
||||||
@@ -187,7 +187,7 @@ exports[`components/sidebar/sidebar_category should match snapshot when isNewCat
|
|||||||
id="sidebar_left.sidebar_category.newLabel"
|
id="sidebar_left.sidebar_category.newLabel"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Connect(Component)
|
<Memo(SidebarCategoryMenu)
|
||||||
category={
|
category={
|
||||||
Object {
|
Object {
|
||||||
"channel_ids": Array [],
|
"channel_ids": Array [],
|
||||||
@@ -279,7 +279,7 @@ exports[`components/sidebar/sidebar_category should match snapshot when sorting
|
|||||||
muted={false}
|
muted={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<Connect(Component)
|
<Memo(SidebarCategorySortingMenu)
|
||||||
category={
|
category={
|
||||||
Object {
|
Object {
|
||||||
"channel_ids": Array [
|
"channel_ids": Array [
|
||||||
@@ -402,7 +402,7 @@ exports[`components/sidebar/sidebar_category should match snapshot when the cate
|
|||||||
muted={false}
|
muted={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<Connect(Component)
|
<Memo(SidebarCategorySortingMenu)
|
||||||
category={
|
category={
|
||||||
Object {
|
Object {
|
||||||
"channel_ids": Array [
|
"channel_ids": Array [
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`components/sidebar/sidebar_category/sidebar_category_menu should match snapshot and contain correct buttons 1`] = `
|
||||||
|
<Memo(SidebarCategoryGenericMenu)
|
||||||
|
id="test_category_id"
|
||||||
|
>
|
||||||
|
<MarkAsUnreadItem
|
||||||
|
handleViewCategory={[Function]}
|
||||||
|
id="test_category_id"
|
||||||
|
numChannels={0}
|
||||||
|
/>
|
||||||
|
<MenuItemSeparator />
|
||||||
|
<MenuItem
|
||||||
|
id="mute-test_category_id"
|
||||||
|
labels={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Mute Category"
|
||||||
|
id="sidebar_left.sidebar_category_menu.muteCategory"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
leadingElement={
|
||||||
|
<BellOutlineIcon
|
||||||
|
size={18}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
aria-haspopup={true}
|
||||||
|
id="rename-test_category_id"
|
||||||
|
labels={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Rename Category"
|
||||||
|
id="sidebar_left.sidebar_category_menu.renameCategory"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
leadingElement={
|
||||||
|
<PencilOutlineIcon
|
||||||
|
size={18}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
aria-haspopup={true}
|
||||||
|
id="delete-test_category_id"
|
||||||
|
isDestructive={true}
|
||||||
|
labels={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Delete Category"
|
||||||
|
id="sidebar_left.sidebar_category_menu.deleteCategory"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
leadingElement={
|
||||||
|
<TrashCanOutlineIcon
|
||||||
|
size={18}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
<MenuItemSeparator />
|
||||||
|
<SubMenu
|
||||||
|
id="sortChannels-test_category_id"
|
||||||
|
labels={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Sort"
|
||||||
|
id="sidebar.sort"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
leadingElement={
|
||||||
|
<SortAlphabeticalAscendingIcon
|
||||||
|
size={18}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
menuAriaLabel="Sort submenu"
|
||||||
|
menuId="sortChannels-test_category_id-menu"
|
||||||
|
trailingElements={
|
||||||
|
<React.Fragment>
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Alphabetically"
|
||||||
|
id="user.settings.sidebar.sortAlpha"
|
||||||
|
/>
|
||||||
|
<ChevronRightIcon
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
id="sortAplhabetical-test_category_id"
|
||||||
|
labels={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Alphabetically"
|
||||||
|
id="user.settings.sidebar.sortAlpha"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
id="sortByMostRecent-test_category_id"
|
||||||
|
labels={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Recent Activity"
|
||||||
|
id="sidebar.sortedByRecencyLabel"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
id="sortManual-test_category_id"
|
||||||
|
labels={
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Manually"
|
||||||
|
id="sidebar.sortedManually"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={[Function]}
|
||||||
|
/>
|
||||||
|
</SubMenu>
|
||||||
|
<MenuItemSeparator />
|
||||||
|
<CreateNewCategoryMenuItem
|
||||||
|
id="test_category_id"
|
||||||
|
/>
|
||||||
|
</Memo(SidebarCategoryGenericMenu)>
|
||||||
|
`;
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`components/sidebar/sidebar_category/sidebar_category_menu should match snapshot and contain correct buttons 1`] = `
|
|
||||||
<div
|
|
||||||
className="SidebarMenu MenuWrapper"
|
|
||||||
>
|
|
||||||
<Menu
|
|
||||||
menu={
|
|
||||||
Object {
|
|
||||||
"aria-label": "Edit category menu",
|
|
||||||
"id": "SidebarChannelMenu-MenuList-test_category_id",
|
|
||||||
"onToggle": [Function],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
menuButton={
|
|
||||||
Object {
|
|
||||||
"aria-label": "Category options",
|
|
||||||
"children": <DotsVerticalIcon
|
|
||||||
size={16}
|
|
||||||
/>,
|
|
||||||
"class": "SidebarMenu_menuButton",
|
|
||||||
"id": "SidebarCategoryMenu-Button-test_category_id",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
menuButtonTooltip={
|
|
||||||
Object {
|
|
||||||
"class": "hidden-xs",
|
|
||||||
"id": "SidebarCategoryMenu-ButtonTooltip-test_category_id",
|
|
||||||
"text": "Category options",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MenuItem
|
|
||||||
id="mute-test_category_id"
|
|
||||||
labels={
|
|
||||||
<Memo(MemoizedFormattedMessage)
|
|
||||||
defaultMessage="Mute Category"
|
|
||||||
id="sidebar_left.sidebar_category_menu.muteCategory"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
leadingElement={
|
|
||||||
<BellOutlineIcon
|
|
||||||
size={18}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={[Function]}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
aria-haspopup={true}
|
|
||||||
id="rename-test_category_id"
|
|
||||||
labels={
|
|
||||||
<Memo(MemoizedFormattedMessage)
|
|
||||||
defaultMessage="Rename Category"
|
|
||||||
id="sidebar_left.sidebar_category_menu.renameCategory"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
leadingElement={
|
|
||||||
<PencilOutlineIcon
|
|
||||||
size={18}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={[Function]}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
aria-haspopup={true}
|
|
||||||
id="delete-test_category_id"
|
|
||||||
isDestructive={true}
|
|
||||||
labels={
|
|
||||||
<Memo(MemoizedFormattedMessage)
|
|
||||||
defaultMessage="Delete Category"
|
|
||||||
id="sidebar_left.sidebar_category_menu.deleteCategory"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
leadingElement={
|
|
||||||
<TrashCanOutlineIcon
|
|
||||||
size={18}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={[Function]}
|
|
||||||
/>
|
|
||||||
<MenuItemSeparator />
|
|
||||||
<SubMenu
|
|
||||||
id="sortChannels-test_category_id"
|
|
||||||
labels={
|
|
||||||
<Memo(MemoizedFormattedMessage)
|
|
||||||
defaultMessage="Sort"
|
|
||||||
id="sidebar.sort"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
leadingElement={
|
|
||||||
<SortAlphabeticalAscendingIcon
|
|
||||||
size={18}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
menuAriaLabel="Sort submenu"
|
|
||||||
menuId="sortChannels-test_category_id-menu"
|
|
||||||
trailingElements={
|
|
||||||
<React.Fragment>
|
|
||||||
<Memo(MemoizedFormattedMessage)
|
|
||||||
defaultMessage="Alphabetically"
|
|
||||||
id="user.settings.sidebar.sortAlpha"
|
|
||||||
/>
|
|
||||||
<ChevronRightIcon
|
|
||||||
size={16}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MenuItem
|
|
||||||
id="sortAplhabetical-test_category_id"
|
|
||||||
labels={
|
|
||||||
<Memo(MemoizedFormattedMessage)
|
|
||||||
defaultMessage="Alphabetically"
|
|
||||||
id="user.settings.sidebar.sortAlpha"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={[Function]}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
id="sortByMostRecent-test_category_id"
|
|
||||||
labels={
|
|
||||||
<Memo(MemoizedFormattedMessage)
|
|
||||||
defaultMessage="Recent Activity"
|
|
||||||
id="sidebar.sortedByRecencyLabel"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={[Function]}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
id="sortManual-test_category_id"
|
|
||||||
labels={
|
|
||||||
<Memo(MemoizedFormattedMessage)
|
|
||||||
defaultMessage="Manually"
|
|
||||||
id="sidebar.sortedManually"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={[Function]}
|
|
||||||
/>
|
|
||||||
</SubMenu>
|
|
||||||
<MenuItemSeparator />
|
|
||||||
<MenuItem
|
|
||||||
aria-haspopup={true}
|
|
||||||
id="create-test_category_id"
|
|
||||||
labels={
|
|
||||||
<Memo(MemoizedFormattedMessage)
|
|
||||||
defaultMessage="Create New Category"
|
|
||||||
id="sidebar_left.sidebar_category_menu.createCategory"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
leadingElement={
|
|
||||||
<FolderPlusOutlineIcon
|
|
||||||
size={18}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={[Function]}
|
|
||||||
/>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback} from 'react';
|
||||||
|
import * as Menu from 'components/menu';
|
||||||
|
|
||||||
|
import {FolderPlusOutlineIcon} from '@mattermost/compass-icons/components';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import {trackEvent} from 'actions/telemetry_actions';
|
||||||
|
import {ModalIdentifiers} from 'utils/constants';
|
||||||
|
|
||||||
|
import EditCategoryModal from 'components/edit_category_modal';
|
||||||
|
import {useDispatch} from 'react-redux';
|
||||||
|
import {openModal} from 'actions/views/modals';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateNewCategoryMenuItem = ({
|
||||||
|
id,
|
||||||
|
}: Props) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const handleCreateCategory = useCallback(() => {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalId: ModalIdentifiers.EDIT_CATEGORY,
|
||||||
|
dialogType: EditCategoryModal,
|
||||||
|
}));
|
||||||
|
trackEvent('ui', 'ui_sidebar_category_menu_createCategory');
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
id={`create-${id}`}
|
||||||
|
onClick={handleCreateCategory}
|
||||||
|
aria-haspopup={true}
|
||||||
|
leadingElement={<FolderPlusOutlineIcon size={18}/>}
|
||||||
|
labels={(
|
||||||
|
<FormattedMessage
|
||||||
|
id='sidebar_left.sidebar_category_menu.createCategory'
|
||||||
|
defaultMessage='Create New Category'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateNewCategoryMenuItem;
|
||||||
@@ -8,7 +8,41 @@ import {CategorySorting} from '@mattermost/types/channel_categories';
|
|||||||
|
|
||||||
import {CategoryTypes} from 'mattermost-redux/constants/channel_categories';
|
import {CategoryTypes} from 'mattermost-redux/constants/channel_categories';
|
||||||
|
|
||||||
import SidebarCategoryMenu from './sidebar_category_menu';
|
import SidebarCategoryMenu from '.';
|
||||||
|
import * as redux from 'react-redux';
|
||||||
|
import CreateNewCategoryMenuItem from './create_new_category_menu_item';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
entities: {
|
||||||
|
preferences: {
|
||||||
|
myPreferences: {},
|
||||||
|
},
|
||||||
|
channels: {
|
||||||
|
channels: {},
|
||||||
|
channelsInTeam: {},
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
currentUserId: '',
|
||||||
|
profiles: {},
|
||||||
|
},
|
||||||
|
teams: {
|
||||||
|
currentTeamId: '',
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
config: {
|
||||||
|
ExperimentalGroupUnreadChannels: 'default_off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
views: {
|
||||||
|
channel: {
|
||||||
|
lastUnreadChannel: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(redux, 'useSelector').mockImplementation((cb) => cb(initialState));
|
||||||
|
jest.spyOn(redux, 'useDispatch').mockReturnValue((t) => t);
|
||||||
|
|
||||||
describe('components/sidebar/sidebar_category/sidebar_category_menu', () => {
|
describe('components/sidebar/sidebar_category/sidebar_category_menu', () => {
|
||||||
const categoryId = 'test_category_id';
|
const categoryId = 'test_category_id';
|
||||||
@@ -24,9 +58,6 @@ describe('components/sidebar/sidebar_category/sidebar_category_menu', () => {
|
|||||||
muted: false,
|
muted: false,
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
},
|
},
|
||||||
openModal: jest.fn(),
|
|
||||||
setCategoryMuted: jest.fn(),
|
|
||||||
setCategorySorting: jest.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should match snapshot and contain correct buttons', () => {
|
test('should match snapshot and contain correct buttons', () => {
|
||||||
@@ -35,7 +66,7 @@ describe('components/sidebar/sidebar_category/sidebar_category_menu', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(wrapper.find(`#rename-${categoryId}`)).toHaveLength(1);
|
expect(wrapper.find(`#rename-${categoryId}`)).toHaveLength(1);
|
||||||
expect(wrapper.find(`#create-${categoryId}`)).toHaveLength(1);
|
expect(wrapper.find(CreateNewCategoryMenuItem)).toHaveLength(1);
|
||||||
expect(wrapper.find(`#delete-${categoryId}`)).toHaveLength(1);
|
expect(wrapper.find(`#delete-${categoryId}`)).toHaveLength(1);
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import {connect, ConnectedProps} from 'react-redux';
|
|
||||||
|
|
||||||
import {setCategoryMuted, setCategorySorting} from 'mattermost-redux/actions/channel_categories';
|
|
||||||
|
|
||||||
import {openModal} from 'actions/views/modals';
|
|
||||||
|
|
||||||
import SidebarCategoryMenu from './sidebar_category_menu';
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
openModal,
|
|
||||||
setCategoryMuted,
|
|
||||||
setCategorySorting,
|
|
||||||
};
|
|
||||||
|
|
||||||
const connector = connect(null, mapDispatchToProps);
|
|
||||||
|
|
||||||
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
|
||||||
|
|
||||||
export default connector(SidebarCategoryMenu);
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {memo, useState} from 'react';
|
import React, {memo, useCallback} from 'react';
|
||||||
import {FormattedMessage, useIntl} from 'react-intl';
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BellOutlineIcon,
|
BellOutlineIcon,
|
||||||
@@ -12,8 +11,6 @@ import {
|
|||||||
FormatListBulletedIcon,
|
FormatListBulletedIcon,
|
||||||
SortAlphabeticalAscendingIcon,
|
SortAlphabeticalAscendingIcon,
|
||||||
ClockOutlineIcon,
|
ClockOutlineIcon,
|
||||||
FolderPlusOutlineIcon,
|
|
||||||
DotsVerticalIcon,
|
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
} from '@mattermost/compass-icons/components';
|
} from '@mattermost/compass-icons/components';
|
||||||
|
|
||||||
@@ -29,32 +26,46 @@ import DeleteCategoryModal from 'components/delete_category_modal';
|
|||||||
import EditCategoryModal from 'components/edit_category_modal';
|
import EditCategoryModal from 'components/edit_category_modal';
|
||||||
import * as Menu from 'components/menu';
|
import * as Menu from 'components/menu';
|
||||||
|
|
||||||
import type {PropsFromRedux} from './index';
|
import SidebarCategoryGenericMenu from './sidebar_category_generic_menu';
|
||||||
|
import MarkAsReadMenuItem from './mark_as_read_menu_item';
|
||||||
|
import CreateNewCategoryMenuItem from './create_new_category_menu_item';
|
||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
import {shouldShowUnreadsCategory} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
import {setCategoryMuted, setCategorySorting} from 'mattermost-redux/actions/channel_categories';
|
||||||
|
|
||||||
type OwnProps = {
|
import {openModal} from 'actions/views/modals';
|
||||||
|
import {makeGetUnreadIdsForCategory} from 'selectors/views/channel_sidebar';
|
||||||
|
import {GlobalState} from 'types/store';
|
||||||
|
import {readMultipleChannels} from 'mattermost-redux/actions/channels';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
category: ChannelCategory;
|
category: ChannelCategory;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = OwnProps & PropsFromRedux;
|
const getUnreadsIdsForCategory = makeGetUnreadIdsForCategory();
|
||||||
|
|
||||||
const SidebarCategoryMenu = (props: Props) => {
|
const SidebarCategoryMenu = ({
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
category,
|
||||||
|
}: Props) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const showUnreadsCategory = useSelector(shouldShowUnreadsCategory);
|
||||||
|
const unreadsIds = useSelector((state: GlobalState) => getUnreadsIdsForCategory(state, category));
|
||||||
|
|
||||||
const {formatMessage} = useIntl();
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
let muteUnmuteCategoryMenuItem: JSX.Element | null = null;
|
let muteUnmuteCategoryMenuItem: JSX.Element | null = null;
|
||||||
if (props.category.type !== CategoryTypes.DIRECT_MESSAGES) {
|
if (category.type !== CategoryTypes.DIRECT_MESSAGES) {
|
||||||
function toggleCategoryMute() {
|
function toggleCategoryMute() {
|
||||||
props.setCategoryMuted(props.category.id, !props.category.muted);
|
dispatch(setCategoryMuted(category.id, !category.muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
muteUnmuteCategoryMenuItem = (
|
muteUnmuteCategoryMenuItem = (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
id={`mute-${props.category.id}`}
|
id={`mute-${category.id}`}
|
||||||
onClick={toggleCategoryMute}
|
onClick={toggleCategoryMute}
|
||||||
leadingElement={<BellOutlineIcon size={18}/>}
|
leadingElement={<BellOutlineIcon size={18}/>}
|
||||||
labels={
|
labels={
|
||||||
props.category.muted ? (
|
category.muted ? (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='sidebar_left.sidebar_category_menu.unmuteCategory'
|
id='sidebar_left.sidebar_category_menu.unmuteCategory'
|
||||||
defaultMessage='Unmute Category'
|
defaultMessage='Unmute Category'
|
||||||
@@ -72,20 +83,20 @@ const SidebarCategoryMenu = (props: Props) => {
|
|||||||
|
|
||||||
let deleteCategoryMenuItem: JSX.Element | null = null;
|
let deleteCategoryMenuItem: JSX.Element | null = null;
|
||||||
let renameCategoryMenuItem: JSX.Element | null = null;
|
let renameCategoryMenuItem: JSX.Element | null = null;
|
||||||
if (props.category.type === CategoryTypes.CUSTOM) {
|
if (category.type === CategoryTypes.CUSTOM) {
|
||||||
function handleDeleteCategory() {
|
function handleDeleteCategory() {
|
||||||
props.openModal({
|
dispatch(openModal({
|
||||||
modalId: ModalIdentifiers.DELETE_CATEGORY,
|
modalId: ModalIdentifiers.DELETE_CATEGORY,
|
||||||
dialogType: DeleteCategoryModal,
|
dialogType: DeleteCategoryModal,
|
||||||
dialogProps: {
|
dialogProps: {
|
||||||
category: props.category,
|
category,
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteCategoryMenuItem = (
|
deleteCategoryMenuItem = (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
id={`delete-${props.category.id}`}
|
id={`delete-${category.id}`}
|
||||||
isDestructive={true}
|
isDestructive={true}
|
||||||
aria-haspopup={true}
|
aria-haspopup={true}
|
||||||
onClick={handleDeleteCategory}
|
onClick={handleDeleteCategory}
|
||||||
@@ -100,19 +111,19 @@ const SidebarCategoryMenu = (props: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
function handleRenameCategory() {
|
function handleRenameCategory() {
|
||||||
props.openModal({
|
dispatch(openModal({
|
||||||
modalId: ModalIdentifiers.EDIT_CATEGORY,
|
modalId: ModalIdentifiers.EDIT_CATEGORY,
|
||||||
dialogType: EditCategoryModal,
|
dialogType: EditCategoryModal,
|
||||||
dialogProps: {
|
dialogProps: {
|
||||||
categoryId: props.category.id,
|
categoryId: category.id,
|
||||||
initialCategoryName: props.category.display_name,
|
initialCategoryName: category.display_name,
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
renameCategoryMenuItem = (
|
renameCategoryMenuItem = (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
id={`rename-${props.category.id}`}
|
id={`rename-${category.id}`}
|
||||||
onClick={handleRenameCategory}
|
onClick={handleRenameCategory}
|
||||||
aria-haspopup={true}
|
aria-haspopup={true}
|
||||||
leadingElement={<PencilOutlineIcon size={18}/>}
|
leadingElement={<PencilOutlineIcon size={18}/>}
|
||||||
@@ -127,7 +138,7 @@ const SidebarCategoryMenu = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleSortChannels(sorting: CategorySorting) {
|
function handleSortChannels(sorting: CategorySorting) {
|
||||||
props.setCategorySorting(props.category.id, sorting);
|
dispatch(setCategorySorting(category.id, sorting));
|
||||||
trackEvent('ui', `ui_sidebar_sort_dm_${sorting}`);
|
trackEvent('ui', `ui_sidebar_sort_dm_${sorting}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +149,7 @@ const SidebarCategoryMenu = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
let sortChannelsIcon = <FormatListBulletedIcon size={18}/>;
|
let sortChannelsIcon = <FormatListBulletedIcon size={18}/>;
|
||||||
if (props.category.sorting === CategorySorting.Alphabetical) {
|
if (category.sorting === CategorySorting.Alphabetical) {
|
||||||
sortChannelsSelectedValue = (
|
sortChannelsSelectedValue = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='user.settings.sidebar.sortAlpha'
|
id='user.settings.sidebar.sortAlpha'
|
||||||
@@ -146,7 +157,7 @@ const SidebarCategoryMenu = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
sortChannelsIcon = <SortAlphabeticalAscendingIcon size={18}/>;
|
sortChannelsIcon = <SortAlphabeticalAscendingIcon size={18}/>;
|
||||||
} else if (props.category.sorting === CategorySorting.Recency) {
|
} else if (category.sorting === CategorySorting.Recency) {
|
||||||
sortChannelsSelectedValue = (
|
sortChannelsSelectedValue = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='user.settings.sidebar.recent'
|
id='user.settings.sidebar.recent'
|
||||||
@@ -158,7 +169,7 @@ const SidebarCategoryMenu = (props: Props) => {
|
|||||||
|
|
||||||
const sortChannelsMenuItem = (
|
const sortChannelsMenuItem = (
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
id={`sortChannels-${props.category.id}`}
|
id={`sortChannels-${category.id}`}
|
||||||
leadingElement={sortChannelsIcon}
|
leadingElement={sortChannelsIcon}
|
||||||
labels={(
|
labels={(
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@@ -172,11 +183,11 @@ const SidebarCategoryMenu = (props: Props) => {
|
|||||||
<ChevronRightIcon size={16}/>
|
<ChevronRightIcon size={16}/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
menuId={`sortChannels-${props.category.id}-menu`}
|
menuId={`sortChannels-${category.id}-menu`}
|
||||||
menuAriaLabel={formatMessage({id: 'sidebar_left.sidebar_category_menu.sort.dropdownAriaLabel', defaultMessage: 'Sort submenu'})}
|
menuAriaLabel={formatMessage({id: 'sidebar_left.sidebar_category_menu.sort.dropdownAriaLabel', defaultMessage: 'Sort submenu'})}
|
||||||
>
|
>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
id={`sortAplhabetical-${props.category.id}`}
|
id={`sortAplhabetical-${category.id}`}
|
||||||
labels={(
|
labels={(
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='user.settings.sidebar.sortAlpha'
|
id='user.settings.sidebar.sortAlpha'
|
||||||
@@ -186,7 +197,7 @@ const SidebarCategoryMenu = (props: Props) => {
|
|||||||
onClick={() => handleSortChannels(CategorySorting.Alphabetical)}
|
onClick={() => handleSortChannels(CategorySorting.Alphabetical)}
|
||||||
/>
|
/>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
id={`sortByMostRecent-${props.category.id}`}
|
id={`sortByMostRecent-${category.id}`}
|
||||||
labels={(
|
labels={(
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='sidebar.sortedByRecencyLabel'
|
id='sidebar.sortedByRecencyLabel'
|
||||||
@@ -196,7 +207,7 @@ const SidebarCategoryMenu = (props: Props) => {
|
|||||||
onClick={() => handleSortChannels(CategorySorting.Recency)}
|
onClick={() => handleSortChannels(CategorySorting.Recency)}
|
||||||
/>
|
/>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
id={`sortManual-${props.category.id}`}
|
id={`sortManual-${category.id}`}
|
||||||
labels={(
|
labels={(
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='sidebar.sortedManually'
|
id='sidebar.sortedManually'
|
||||||
@@ -208,71 +219,33 @@ const SidebarCategoryMenu = (props: Props) => {
|
|||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleCreateCategory() {
|
const handleViewCategory = useCallback(() => {
|
||||||
props.openModal({
|
dispatch(readMultipleChannels(unreadsIds));
|
||||||
modalId: ModalIdentifiers.EDIT_CATEGORY,
|
trackEvent('ui', 'ui_sidebar_category_menu_viewCategory');
|
||||||
dialogType: EditCategoryModal,
|
}, [dispatch, unreadsIds]);
|
||||||
});
|
|
||||||
trackEvent('ui', 'ui_sidebar_category_menu_createCategory');
|
|
||||||
}
|
|
||||||
|
|
||||||
const createNewCategoryMenuItem = (
|
const markAsReadMenuItem = showUnreadsCategory ?
|
||||||
<Menu.Item
|
null :
|
||||||
id={`create-${props.category.id}`}
|
(
|
||||||
onClick={handleCreateCategory}
|
<MarkAsReadMenuItem
|
||||||
aria-haspopup={true}
|
id={category.id}
|
||||||
leadingElement={<FolderPlusOutlineIcon size={18}/>}
|
handleViewCategory={handleViewCategory}
|
||||||
labels={(
|
numChannels={unreadsIds.length}
|
||||||
<FormattedMessage
|
/>
|
||||||
id='sidebar_left.sidebar_category_menu.createCategory'
|
);
|
||||||
defaultMessage='Create New Category'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
function handleMenuToggle(isOpen: boolean) {
|
|
||||||
setIsMenuOpen(isOpen);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<SidebarCategoryGenericMenu id={category.id}>
|
||||||
className={classNames(
|
{markAsReadMenuItem}
|
||||||
'SidebarMenu',
|
{markAsReadMenuItem && <Menu.Separator/>}
|
||||||
'MenuWrapper',
|
{muteUnmuteCategoryMenuItem}
|
||||||
{
|
{renameCategoryMenuItem}
|
||||||
'MenuWrapper--open': isMenuOpen,
|
{deleteCategoryMenuItem}
|
||||||
menuOpen: isMenuOpen,
|
<Menu.Separator/>
|
||||||
},
|
{sortChannelsMenuItem}
|
||||||
)}
|
<Menu.Separator/>
|
||||||
>
|
<CreateNewCategoryMenuItem id={category.id}/>
|
||||||
<Menu.Container
|
</SidebarCategoryGenericMenu>
|
||||||
menuButton={{
|
|
||||||
id: `SidebarCategoryMenu-Button-${props.category.id}`,
|
|
||||||
'aria-label': formatMessage({id: 'sidebar_left.sidebar_category_menu.editCategory', defaultMessage: 'Category options'}),
|
|
||||||
class: 'SidebarMenu_menuButton',
|
|
||||||
children: <DotsVerticalIcon size={16}/>,
|
|
||||||
}}
|
|
||||||
menuButtonTooltip={{
|
|
||||||
id: `SidebarCategoryMenu-ButtonTooltip-${props.category.id}`,
|
|
||||||
text: formatMessage({id: 'sidebar_left.sidebar_category_menu.editCategory', defaultMessage: 'Category options'}),
|
|
||||||
class: 'hidden-xs',
|
|
||||||
}}
|
|
||||||
menu={{
|
|
||||||
id: `SidebarChannelMenu-MenuList-${props.category.id}`,
|
|
||||||
'aria-label': formatMessage({id: 'sidebar_left.sidebar_category_menu.dropdownAriaLabel', defaultMessage: 'Edit category menu'}),
|
|
||||||
onToggle: handleMenuToggle,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{muteUnmuteCategoryMenuItem}
|
|
||||||
{renameCategoryMenuItem}
|
|
||||||
{deleteCategoryMenuItem}
|
|
||||||
<Menu.Separator/>
|
|
||||||
{sortChannelsMenuItem}
|
|
||||||
<Menu.Separator/>
|
|
||||||
{createNewCategoryMenuItem}
|
|
||||||
</Menu.Container>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
|
||||||
|
import {GenericModal} from '@mattermost/components';
|
||||||
|
|
||||||
|
import 'components/category_modal.scss';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleConfirm: () => void;
|
||||||
|
numChannels: number;
|
||||||
|
onExited: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => null;
|
||||||
|
|
||||||
|
const MarkAsReadConfirmModal = ({
|
||||||
|
handleConfirm,
|
||||||
|
numChannels,
|
||||||
|
onExited,
|
||||||
|
}: Props) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const header = intl.formatMessage({id: 'mark_as_read_confirm_modal.header', defaultMessage: 'Mark as read'});
|
||||||
|
const body = intl.formatMessage({id: 'mark_as_read_confirm_modal.body', defaultMessage: 'Are you sure you want to mark {numChannels} channels as read?'}, {numChannels});
|
||||||
|
const confirm = intl.formatMessage({id: 'mark_as_read_confirm_modal.confirm', defaultMessage: 'Mark as read'});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericModal
|
||||||
|
ariaLabel={header}
|
||||||
|
modalHeaderText={header}
|
||||||
|
handleConfirm={handleConfirm}
|
||||||
|
handleCancel={handleCancel}
|
||||||
|
onExited={onExited}
|
||||||
|
confirmButtonText={confirm}
|
||||||
|
>
|
||||||
|
<span className='mark-as-read__helpText'>
|
||||||
|
{body}
|
||||||
|
</span>
|
||||||
|
</GenericModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarkAsReadConfirmModal;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback} from 'react';
|
||||||
|
import * as Menu from 'components/menu';
|
||||||
|
|
||||||
|
import {MarkAsUnreadIcon} from '@mattermost/compass-icons/components';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import {useDispatch} from 'react-redux';
|
||||||
|
import {openModal} from 'actions/views/modals';
|
||||||
|
import {ModalIdentifiers} from 'utils/constants';
|
||||||
|
import MarkAsReadConfirmModal from './mark_as_read_confirm_modal';
|
||||||
|
|
||||||
|
type Props = ({
|
||||||
|
id: string;
|
||||||
|
handleViewCategory: () => void;
|
||||||
|
numChannels: number;
|
||||||
|
})
|
||||||
|
|
||||||
|
const MarkAsUnreadItem = ({
|
||||||
|
id,
|
||||||
|
handleViewCategory,
|
||||||
|
numChannels,
|
||||||
|
}: Props) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
if (numChannels <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numChannels === 1) {
|
||||||
|
handleViewCategory();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(openModal({
|
||||||
|
modalId: ModalIdentifiers.DELETE_CATEGORY,
|
||||||
|
dialogType: MarkAsReadConfirmModal,
|
||||||
|
dialogProps: {
|
||||||
|
handleConfirm: handleViewCategory,
|
||||||
|
numChannels,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, [dispatch, handleViewCategory, numChannels]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
id={`view-${id}`}
|
||||||
|
onClick={onClick}
|
||||||
|
aria-haspopup={numChannels > 1}
|
||||||
|
leadingElement={<MarkAsUnreadIcon size={18}/>}
|
||||||
|
labels={(
|
||||||
|
<FormattedMessage
|
||||||
|
id='sidebar_left.sidebar_category_menu.viewCategory'
|
||||||
|
defaultMessage='Mark category as read'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarkAsUnreadItem;
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {memo, useState} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DotsVerticalIcon,
|
||||||
|
} from '@mattermost/compass-icons/components';
|
||||||
|
|
||||||
|
import * as Menu from 'components/menu';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
children: React.ReactNode[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const SidebarCategoryGenericMenu = ({
|
||||||
|
id,
|
||||||
|
children,
|
||||||
|
}: Props) => {
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
|
function handleMenuToggle(isOpen: boolean) {
|
||||||
|
setIsMenuOpen(isOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'SidebarMenu',
|
||||||
|
'MenuWrapper',
|
||||||
|
{
|
||||||
|
'MenuWrapper--open': isMenuOpen,
|
||||||
|
menuOpen: isMenuOpen,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Menu.Container
|
||||||
|
menuButton={{
|
||||||
|
id: `SidebarCategoryMenu-Button-${id}`,
|
||||||
|
'aria-label': formatMessage({id: 'sidebar_left.sidebar_category_menu.editCategory', defaultMessage: 'Category options'}),
|
||||||
|
class: 'SidebarMenu_menuButton',
|
||||||
|
children: <DotsVerticalIcon size={16}/>,
|
||||||
|
}}
|
||||||
|
menuButtonTooltip={{
|
||||||
|
id: `SidebarCategoryMenu-ButtonTooltip-${id}`,
|
||||||
|
text: formatMessage({id: 'sidebar_left.sidebar_category_menu.editCategory', defaultMessage: 'Category options'}),
|
||||||
|
class: 'hidden-xs',
|
||||||
|
}}
|
||||||
|
menu={{
|
||||||
|
id: `SidebarChannelMenu-MenuList-${id}`,
|
||||||
|
'aria-label': formatMessage({id: 'sidebar_left.sidebar_category_menu.dropdownAriaLabel', defaultMessage: 'Edit category menu'}),
|
||||||
|
onToggle: handleMenuToggle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Menu.Container>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(SidebarCategoryGenericMenu);
|
||||||
@@ -2,21 +2,35 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import * as redux from 'react-redux';
|
||||||
import {shallow} from 'enzyme';
|
import {shallow} from 'enzyme';
|
||||||
|
|
||||||
import {TestHelper} from 'utils/test_helper';
|
import {TestHelper} from 'utils/test_helper';
|
||||||
import Constants from 'utils/constants';
|
|
||||||
|
|
||||||
import SidebarCategorySortingMenu from './sidebar_category_sorting_menu';
|
import SidebarCategorySortingMenu from './sidebar_category_sorting_menu';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
entities: {
|
||||||
|
users: {
|
||||||
|
currentUserId: 'user_id',
|
||||||
|
},
|
||||||
|
preferences: {
|
||||||
|
myPreferences: {
|
||||||
|
'sidebar_settings--limit_visible_dms_gms': {
|
||||||
|
value: '10',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(redux, 'useSelector').mockImplementation((cb) => cb(initialState));
|
||||||
|
jest.spyOn(redux, 'useDispatch').mockReturnValue((t) => t);
|
||||||
|
|
||||||
describe('components/sidebar/sidebar_category/sidebar_category_sorting_menu', () => {
|
describe('components/sidebar/sidebar_category/sidebar_category_sorting_menu', () => {
|
||||||
const baseProps = {
|
const baseProps = {
|
||||||
category: TestHelper.getCategoryMock(),
|
category: TestHelper.getCategoryMock(),
|
||||||
handleOpenDirectMessagesModal: jest.fn(),
|
handleOpenDirectMessagesModal: jest.fn(),
|
||||||
selectedDmNumber: Constants.DM_AND_GM_SHOW_COUNTS[0],
|
|
||||||
currentUserId: TestHelper.getUserMock().id,
|
|
||||||
setCategorySorting: jest.fn(),
|
|
||||||
savePreferences: jest.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should match snapshot', () => {
|
test('should match snapshot', () => {
|
||||||
@@ -24,21 +24,30 @@ import {trackEvent} from 'actions/telemetry_actions';
|
|||||||
|
|
||||||
import * as Menu from 'components/menu';
|
import * as Menu from 'components/menu';
|
||||||
|
|
||||||
import type {PropsFromRedux} from './index';
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
import {getVisibleDmGmLimit} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
import {setCategorySorting} from 'mattermost-redux/actions/channel_categories';
|
||||||
|
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||||
|
|
||||||
type OwnProps = {
|
type Props = {
|
||||||
category: ChannelCategory;
|
category: ChannelCategory;
|
||||||
handleOpenDirectMessagesModal: (e: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) => void;
|
handleOpenDirectMessagesModal: (e: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = OwnProps & PropsFromRedux;
|
const SidebarCategorySortingMenu = ({
|
||||||
|
category,
|
||||||
const SidebarCategorySortingMenu = (props: Props) => {
|
handleOpenDirectMessagesModal,
|
||||||
|
}: Props) => {
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
const {formatMessage} = useIntl();
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const selectedDmNumber = useSelector(getVisibleDmGmLimit);
|
||||||
|
const currentUserId = useSelector(getCurrentUserId);
|
||||||
|
|
||||||
function handleSortDirectMessages(sorting: CategorySorting) {
|
function handleSortDirectMessages(sorting: CategorySorting) {
|
||||||
props.setCategorySorting(props.category.id, sorting);
|
dispatch(setCategorySorting(category.id, sorting));
|
||||||
trackEvent('ui', `ui_sidebar_sort_dm_${sorting}`);
|
trackEvent('ui', `ui_sidebar_sort_dm_${sorting}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +58,7 @@ const SidebarCategorySortingMenu = (props: Props) => {
|
|||||||
defaultMessage='Recent Activity'
|
defaultMessage='Recent Activity'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
if (props.category.sorting === CategorySorting.Alphabetical) {
|
if (category.sorting === CategorySorting.Alphabetical) {
|
||||||
sortDirectMessagesSelectedValue = (
|
sortDirectMessagesSelectedValue = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='user.settings.sidebar.sortAlpha'
|
id='user.settings.sidebar.sortAlpha'
|
||||||
@@ -61,7 +70,7 @@ const SidebarCategorySortingMenu = (props: Props) => {
|
|||||||
|
|
||||||
const sortDirectMessagesMenuItem = (
|
const sortDirectMessagesMenuItem = (
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
id={`sortDirectMessages-${props.category.id}`}
|
id={`sortDirectMessages-${category.id}`}
|
||||||
leadingElement={sortDirectMessagesIcon}
|
leadingElement={sortDirectMessagesIcon}
|
||||||
labels={(
|
labels={(
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@@ -75,10 +84,10 @@ const SidebarCategorySortingMenu = (props: Props) => {
|
|||||||
<ChevronRightIcon size={16}/>
|
<ChevronRightIcon size={16}/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
menuId={`sortDirectMessages-${props.category.id}-menu`}
|
menuId={`sortDirectMessages-${category.id}-menu`}
|
||||||
>
|
>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
id={`sortAlphabetical-${props.category.id}`}
|
id={`sortAlphabetical-${category.id}`}
|
||||||
labels={(
|
labels={(
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='user.settings.sidebar.sortAlpha'
|
id='user.settings.sidebar.sortAlpha'
|
||||||
@@ -88,7 +97,7 @@ const SidebarCategorySortingMenu = (props: Props) => {
|
|||||||
onClick={() => handleSortDirectMessages(CategorySorting.Alphabetical)}
|
onClick={() => handleSortDirectMessages(CategorySorting.Alphabetical)}
|
||||||
/>
|
/>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
id={`sortByMostRecent-${props.category.id}`}
|
id={`sortByMostRecent-${category.id}`}
|
||||||
labels={(
|
labels={(
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='sidebar.sortedByRecencyLabel'
|
id='sidebar.sortedByRecencyLabel'
|
||||||
@@ -102,16 +111,16 @@ const SidebarCategorySortingMenu = (props: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
function handlelimitVisibleDMsGMs(number: number) {
|
function handlelimitVisibleDMsGMs(number: number) {
|
||||||
props.savePreferences(props.currentUserId, [{
|
dispatch(savePreferences(currentUserId, [{
|
||||||
user_id: props.currentUserId,
|
user_id: currentUserId,
|
||||||
category: Constants.Preferences.CATEGORY_SIDEBAR_SETTINGS,
|
category: Constants.Preferences.CATEGORY_SIDEBAR_SETTINGS,
|
||||||
name: Preferences.LIMIT_VISIBLE_DMS_GMS,
|
name: Preferences.LIMIT_VISIBLE_DMS_GMS,
|
||||||
value: number.toString(),
|
value: number.toString(),
|
||||||
}]);
|
}]));
|
||||||
}
|
}
|
||||||
|
|
||||||
let showMessagesCountSelectedValue = <span>{props.selectedDmNumber}</span>;
|
let showMessagesCountSelectedValue = <span>{selectedDmNumber}</span>;
|
||||||
if (props.selectedDmNumber === 10000) {
|
if (selectedDmNumber === 10000) {
|
||||||
showMessagesCountSelectedValue = (
|
showMessagesCountSelectedValue = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='channel_notifications.levels.all'
|
id='channel_notifications.levels.all'
|
||||||
@@ -122,7 +131,7 @@ const SidebarCategorySortingMenu = (props: Props) => {
|
|||||||
|
|
||||||
const showMessagesCountMenuItem = (
|
const showMessagesCountMenuItem = (
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
id={`showMessagesCount-${props.category.id}`}
|
id={`showMessagesCount-${category.id}`}
|
||||||
leadingElement={<AccountMultipleOutlineIcon size={18}/>}
|
leadingElement={<AccountMultipleOutlineIcon size={18}/>}
|
||||||
labels={(
|
labels={(
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@@ -136,10 +145,10 @@ const SidebarCategorySortingMenu = (props: Props) => {
|
|||||||
<ChevronRightIcon size={16}/>
|
<ChevronRightIcon size={16}/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
menuId={`showMessagesCount-${props.category.id}-menu`}
|
menuId={`showMessagesCount-${category.id}-menu`}
|
||||||
>
|
>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
id={`showAllDms-${props.category.id}`}
|
id={`showAllDms-${category.id}`}
|
||||||
labels={(
|
labels={(
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='sidebar.allDirectMessages'
|
id='sidebar.allDirectMessages'
|
||||||
@@ -151,8 +160,8 @@ const SidebarCategorySortingMenu = (props: Props) => {
|
|||||||
<Menu.Separator/>
|
<Menu.Separator/>
|
||||||
{Constants.DM_AND_GM_SHOW_COUNTS.map((dmGmShowCount) => (
|
{Constants.DM_AND_GM_SHOW_COUNTS.map((dmGmShowCount) => (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
id={`showDmCount-${props.category.id}-${dmGmShowCount}`}
|
id={`showDmCount-${category.id}-${dmGmShowCount}`}
|
||||||
key={`showDmCount-${props.category.id}-${dmGmShowCount}`}
|
key={`showDmCount-${category.id}-${dmGmShowCount}`}
|
||||||
labels={<span>{dmGmShowCount}</span>}
|
labels={<span>{dmGmShowCount}</span>}
|
||||||
onClick={() => handlelimitVisibleDMsGMs(dmGmShowCount)}
|
onClick={() => handlelimitVisibleDMsGMs(dmGmShowCount)}
|
||||||
/>
|
/>
|
||||||
@@ -163,8 +172,8 @@ const SidebarCategorySortingMenu = (props: Props) => {
|
|||||||
|
|
||||||
const openDirectMessageMenuItem = (
|
const openDirectMessageMenuItem = (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
id={`openDirectMessage-${props.category.id}`}
|
id={`openDirectMessage-${category.id}`}
|
||||||
onClick={props.handleOpenDirectMessagesModal}
|
onClick={handleOpenDirectMessagesModal}
|
||||||
leadingElement={<AccountPlusOutlineIcon size={18}/>}
|
leadingElement={<AccountPlusOutlineIcon size={18}/>}
|
||||||
labels={(
|
labels={(
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@@ -190,18 +199,18 @@ const SidebarCategorySortingMenu = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<Menu.Container
|
<Menu.Container
|
||||||
menuButton={{
|
menuButton={{
|
||||||
id: `SidebarCategorySortingMenu-Button-${props.category.id}`,
|
id: `SidebarCategorySortingMenu-Button-${category.id}`,
|
||||||
'aria-label': formatMessage({id: 'sidebar_left.sidebar_category_menu.editCategory', defaultMessage: 'Category options'}),
|
'aria-label': formatMessage({id: 'sidebar_left.sidebar_category_menu.editCategory', defaultMessage: 'Category options'}),
|
||||||
class: 'SidebarMenu_menuButton sortingMenu',
|
class: 'SidebarMenu_menuButton sortingMenu',
|
||||||
children: <DotsVerticalIcon size={16}/>,
|
children: <DotsVerticalIcon size={16}/>,
|
||||||
}}
|
}}
|
||||||
menuButtonTooltip={{
|
menuButtonTooltip={{
|
||||||
id: `SidebarCategorySortingMenu-ButtonTooltip-${props.category.id}`,
|
id: `SidebarCategorySortingMenu-ButtonTooltip-${category.id}`,
|
||||||
text: formatMessage({id: 'sidebar_left.sidebar_category_menu.editCategory', defaultMessage: 'Category options'}),
|
text: formatMessage({id: 'sidebar_left.sidebar_category_menu.editCategory', defaultMessage: 'Category options'}),
|
||||||
class: 'hidden-xs',
|
class: 'hidden-xs',
|
||||||
}}
|
}}
|
||||||
menu={{
|
menu={{
|
||||||
id: `SidebarCategorySortingMenu-MenuList-${props.category.id}`,
|
id: `SidebarCategorySortingMenu-MenuList-${category.id}`,
|
||||||
'aria-label': formatMessage({id: 'sidebar_left.sidebar_category_menu.dropdownAriaLabel', defaultMessage: 'Edit category menu'}),
|
'aria-label': formatMessage({id: 'sidebar_left.sidebar_category_menu.dropdownAriaLabel', defaultMessage: 'Edit category menu'}),
|
||||||
onToggle: handleMenuToggle,
|
onToggle: handleMenuToggle,
|
||||||
}}
|
}}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import {connect, ConnectedProps} from 'react-redux';
|
|
||||||
|
|
||||||
import {setCategorySorting} from 'mattermost-redux/actions/channel_categories';
|
|
||||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
|
||||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
|
||||||
import {getVisibleDmGmLimit} from 'mattermost-redux/selectors/entities/preferences';
|
|
||||||
|
|
||||||
import {GlobalState} from 'types/store';
|
|
||||||
|
|
||||||
import SidebarCategorySortingMenu from './sidebar_category_sorting_menu';
|
|
||||||
|
|
||||||
function mapStateToProps(state: GlobalState) {
|
|
||||||
return {
|
|
||||||
selectedDmNumber: getVisibleDmGmLimit(state),
|
|
||||||
currentUserId: getCurrentUserId(state),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
setCategorySorting,
|
|
||||||
savePreferences,
|
|
||||||
};
|
|
||||||
|
|
||||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
|
||||||
|
|
||||||
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
|
||||||
|
|
||||||
export default connector(SidebarCategorySortingMenu);
|
|
||||||
@@ -37,7 +37,7 @@ const SidebarChannelMenu = (props: Props) => {
|
|||||||
let markAsReadUnreadMenuItem: JSX.Element | null = null;
|
let markAsReadUnreadMenuItem: JSX.Element | null = null;
|
||||||
if (props.isUnread) {
|
if (props.isUnread) {
|
||||||
function handleMarkAsRead() {
|
function handleMarkAsRead() {
|
||||||
props.markChannelAsRead(props.channel.id);
|
props.markChannelAsRead(props.channel.id, true);
|
||||||
trackEvent('ui', 'ui_sidebar_channel_menu_markAsRead');
|
trackEvent('ui', 'ui_sidebar_channel_menu_markAsRead');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
setDraggingState,
|
setDraggingState,
|
||||||
stopDragging,
|
stopDragging,
|
||||||
clearChannelSelection,
|
clearChannelSelection,
|
||||||
multiSelectChannelAdd,
|
|
||||||
} from 'actions/views/channel_sidebar';
|
} from 'actions/views/channel_sidebar';
|
||||||
import {close, switchToLhsStaticPage} from 'actions/views/lhs';
|
import {close, switchToLhsStaticPage} from 'actions/views/lhs';
|
||||||
import {
|
import {
|
||||||
@@ -68,7 +67,6 @@ function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
|||||||
setDraggingState,
|
setDraggingState,
|
||||||
stopDragging,
|
stopDragging,
|
||||||
clearChannelSelection,
|
clearChannelSelection,
|
||||||
multiSelectChannelAdd,
|
|
||||||
switchToLhsStaticPage,
|
switchToLhsStaticPage,
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ type Props = {
|
|||||||
setDraggingState: (data: DraggingState) => void;
|
setDraggingState: (data: DraggingState) => void;
|
||||||
stopDragging: () => void;
|
stopDragging: () => void;
|
||||||
clearChannelSelection: () => void;
|
clearChannelSelection: () => void;
|
||||||
multiSelectChannelAdd: (channelId: string) => void;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
74
webapp/channels/src/components/sidebar/unread_channels.tsx
Обычный файл
74
webapp/channels/src/components/sidebar/unread_channels.tsx
Обычный файл
@@ -0,0 +1,74 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
|
||||||
|
import {SidebarCategoryHeaderStatic} from './sidebar_category_header';
|
||||||
|
import SidebarChannel from './sidebar_channel';
|
||||||
|
import SidebarCategoryGenericMenu from './sidebar_category/sidebar_category_menu/sidebar_category_generic_menu';
|
||||||
|
import MarkAsReadMenuItem from './sidebar_category/sidebar_category_menu/mark_as_read_menu_item';
|
||||||
|
import * as Menu from 'components/menu';
|
||||||
|
import CreateNewCategoryMenuItem from './sidebar_category/sidebar_category_menu/create_new_category_menu_item';
|
||||||
|
import {trackEvent} from 'actions/telemetry_actions';
|
||||||
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
|
import {getUnreadChannels} from 'selectors/views/channel_sidebar';
|
||||||
|
import {readMultipleChannels} from 'mattermost-redux/actions/channels';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setChannelRef: (channelId: string, ref: HTMLLIElement) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function UnreadChannels({
|
||||||
|
setChannelRef,
|
||||||
|
}: Props) {
|
||||||
|
const intl = useIntl();
|
||||||
|
const unreadChannels = useSelector(getUnreadChannels);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleViewCategory = useCallback(() => {
|
||||||
|
if (!unreadChannels.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(readMultipleChannels(unreadChannels.map((v) => v.id)));
|
||||||
|
trackEvent('ui', 'ui_sidebar_category_menu_viewUnreadCategory');
|
||||||
|
}, [unreadChannels, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='SidebarChannelGroup dropDisabled a11y__section'>
|
||||||
|
<SidebarCategoryHeaderStatic displayName={intl.formatMessage({id: 'sidebar.types.unreads', defaultMessage: 'UNREADS'})}>
|
||||||
|
<SidebarCategoryGenericMenu id='unreads'>
|
||||||
|
<MarkAsReadMenuItem
|
||||||
|
id={'unreads'}
|
||||||
|
handleViewCategory={handleViewCategory}
|
||||||
|
numChannels={unreadChannels.length}
|
||||||
|
/>
|
||||||
|
<Menu.Separator/>
|
||||||
|
<CreateNewCategoryMenuItem id={'unreads'}/>
|
||||||
|
</SidebarCategoryGenericMenu>
|
||||||
|
</SidebarCategoryHeaderStatic>
|
||||||
|
<div className='SidebarChannelGroup_content'>
|
||||||
|
<ul
|
||||||
|
role='list'
|
||||||
|
className='NavGroupContent'
|
||||||
|
>
|
||||||
|
{unreadChannels.map((channel, index) => {
|
||||||
|
return (
|
||||||
|
<SidebarChannel
|
||||||
|
key={channel.id}
|
||||||
|
channelIndex={index}
|
||||||
|
channelId={channel.id}
|
||||||
|
setChannelRef={setChannelRef}
|
||||||
|
isCategoryCollapsed={false}
|
||||||
|
isCategoryDragged={false}
|
||||||
|
isDraggable={false}
|
||||||
|
isAutoSortedCategory={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import {connect} from 'react-redux';
|
|
||||||
|
|
||||||
import {getUnreadChannels} from 'selectors/views/channel_sidebar';
|
|
||||||
|
|
||||||
import {GlobalState} from 'types/store';
|
|
||||||
|
|
||||||
import UnreadChannels from './unread_channels';
|
|
||||||
|
|
||||||
function mapStateToProps(state: GlobalState) {
|
|
||||||
return {
|
|
||||||
unreadChannels: getUnreadChannels(state),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(UnreadChannels);
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import {useIntl} from 'react-intl';
|
|
||||||
|
|
||||||
import {Channel} from '@mattermost/types/channels';
|
|
||||||
|
|
||||||
import {SidebarCategoryHeaderStatic} from '../sidebar_category_header';
|
|
||||||
import SidebarChannel from '../sidebar_channel';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
setChannelRef: (channelId: string, ref: HTMLLIElement) => void;
|
|
||||||
unreadChannels: Channel[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function UnreadChannels(props: Props) {
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
if (props.unreadChannels.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='SidebarChannelGroup dropDisabled a11y__section'>
|
|
||||||
<SidebarCategoryHeaderStatic displayName={intl.formatMessage({id: 'sidebar.types.unreads', defaultMessage: 'UNREADS'})}/>
|
|
||||||
<div className='SidebarChannelGroup_content'>
|
|
||||||
<ul
|
|
||||||
role='list'
|
|
||||||
className='NavGroupContent'
|
|
||||||
>
|
|
||||||
{props.unreadChannels.map((channel, index) => {
|
|
||||||
return (
|
|
||||||
<SidebarChannel
|
|
||||||
key={channel.id}
|
|
||||||
channelIndex={index}
|
|
||||||
channelId={channel.id}
|
|
||||||
setChannelRef={props.setChannelRef}
|
|
||||||
isCategoryCollapsed={false}
|
|
||||||
isCategoryDragged={false}
|
|
||||||
isDraggable={false}
|
|
||||||
isAutoSortedCategory={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
import {connect, ConnectedProps} from 'react-redux';
|
import {connect, ConnectedProps} from 'react-redux';
|
||||||
import {RouteComponentProps} from 'react-router-dom';
|
import {RouteComponentProps} from 'react-router-dom';
|
||||||
|
|
||||||
import {fetchAllMyTeamsChannelsAndChannelMembersREST, fetchMyChannelsAndMembersREST, viewChannel} from 'mattermost-redux/actions/channels';
|
import {fetchAllMyTeamsChannelsAndChannelMembersREST, fetchMyChannelsAndMembersREST} from 'mattermost-redux/actions/channels';
|
||||||
import {getLicense, getConfig} from 'mattermost-redux/selectors/entities/general';
|
import {getLicense, getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||||
import {getCurrentTeamId, getMyTeams} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeamId, getMyTeams} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||||
@@ -39,7 +39,6 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
|||||||
const disableRefetchingOnBrowserFocus = config.DisableRefetchingOnBrowserFocus === 'true';
|
const disableRefetchingOnBrowserFocus = config.DisableRefetchingOnBrowserFocus === 'true';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentUser,
|
|
||||||
currentTeamId: getCurrentTeamId(state),
|
currentTeamId: getCurrentTeamId(state),
|
||||||
currentChannelId: getCurrentChannelId(state),
|
currentChannelId: getCurrentChannelId(state),
|
||||||
teamsList: getMyTeams(state),
|
teamsList: getMyTeams(state),
|
||||||
@@ -55,7 +54,6 @@ const mapDispatchToProps = {
|
|||||||
fetchChannelsAndMembers,
|
fetchChannelsAndMembers,
|
||||||
fetchMyChannelsAndMembersREST,
|
fetchMyChannelsAndMembersREST,
|
||||||
fetchAllMyTeamsChannelsAndChannelMembersREST,
|
fetchAllMyTeamsChannelsAndChannelMembersREST,
|
||||||
viewChannel,
|
|
||||||
markChannelAsReadOnFocus,
|
markChannelAsReadOnFocus,
|
||||||
initializeTeam,
|
initializeTeam,
|
||||||
joinTeam,
|
joinTeam,
|
||||||
|
|||||||
@@ -108,10 +108,6 @@ function TeamController(props: Props) {
|
|||||||
function handleBlur() {
|
function handleBlur() {
|
||||||
window.isActive = false;
|
window.isActive = false;
|
||||||
blurTime.current = Date.now();
|
blurTime.current = Date.now();
|
||||||
|
|
||||||
if (props.currentUser) {
|
|
||||||
props.viewChannel('');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeydown(event: KeyboardEvent) {
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
@@ -137,7 +133,7 @@ function TeamController(props: Props) {
|
|||||||
window.removeEventListener('blur', handleBlur);
|
window.removeEventListener('blur', handleBlur);
|
||||||
window.removeEventListener('keydown', handleKeydown);
|
window.removeEventListener('keydown', handleKeydown);
|
||||||
};
|
};
|
||||||
}, [props.selectedThreadId, props.graphQLEnabled, props.currentChannelId, props.currentTeamId, props.currentUser.id]);
|
}, [props.selectedThreadId, props.graphQLEnabled, props.currentChannelId, props.currentTeamId]);
|
||||||
|
|
||||||
// Effect runs on mount, adds active state to window
|
// Effect runs on mount, adds active state to window
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -3922,6 +3922,9 @@
|
|||||||
"mark_all_threads_as_read_modal.confirm": "Mark all as read",
|
"mark_all_threads_as_read_modal.confirm": "Mark all as read",
|
||||||
"mark_all_threads_as_read_modal.description": "This will clear the unread state and mention badges on all your threads. Are you sure?",
|
"mark_all_threads_as_read_modal.description": "This will clear the unread state and mention badges on all your threads. Are you sure?",
|
||||||
"mark_all_threads_as_read_modal.title": "Mark all your threads as read?",
|
"mark_all_threads_as_read_modal.title": "Mark all your threads as read?",
|
||||||
|
"mark_as_read_confirm_modal.body": "Are you sure you want to mark {numChannels} channels as read?",
|
||||||
|
"mark_as_read_confirm_modal.confirm": "Mark as read",
|
||||||
|
"mark_as_read_confirm_modal.header": "Mark as read",
|
||||||
"marketplace_command.disabled": "The marketplace is disabled. Please contact your System Administrator for details.",
|
"marketplace_command.disabled": "The marketplace is disabled. Please contact your System Administrator for details.",
|
||||||
"marketplace_command.no_permission": "You do not have the appropriate permissions to access the marketplace.",
|
"marketplace_command.no_permission": "You do not have the appropriate permissions to access the marketplace.",
|
||||||
"marketplace_modal_list.no_plugins_filter": "No results for \"{filter}\"",
|
"marketplace_modal_list.no_plugins_filter": "No results for \"{filter}\"",
|
||||||
@@ -4798,6 +4801,7 @@
|
|||||||
"sidebar_left.sidebar_category_menu.renameCategory": "Rename Category",
|
"sidebar_left.sidebar_category_menu.renameCategory": "Rename Category",
|
||||||
"sidebar_left.sidebar_category_menu.sort.dropdownAriaLabel": "Sort submenu",
|
"sidebar_left.sidebar_category_menu.sort.dropdownAriaLabel": "Sort submenu",
|
||||||
"sidebar_left.sidebar_category_menu.unmuteCategory": "Unmute Category",
|
"sidebar_left.sidebar_category_menu.unmuteCategory": "Unmute Category",
|
||||||
|
"sidebar_left.sidebar_category_menu.viewCategory": "Mark category as read",
|
||||||
"sidebar_left.sidebar_category.newDropBoxLabel": "Drag channels here...",
|
"sidebar_left.sidebar_category.newDropBoxLabel": "Drag channels here...",
|
||||||
"sidebar_left.sidebar_category.newLabel": "new",
|
"sidebar_left.sidebar_category.newLabel": "new",
|
||||||
"sidebar_left.sidebar_channel_menu.addMembers": "Add Members",
|
"sidebar_left.sidebar_channel_menu.addMembers": "Add Members",
|
||||||
|
|||||||
@@ -637,364 +637,6 @@ describe('Actions.Channels', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('viewChannel', () => {
|
|
||||||
test('should contact server and update last_viewed_at of both channels', async () => {
|
|
||||||
const channelId = TestHelper.generateId();
|
|
||||||
const prevChannelId = TestHelper.generateId();
|
|
||||||
|
|
||||||
const currentUserId = TestHelper.generateId();
|
|
||||||
|
|
||||||
store = configureStore({
|
|
||||||
entities: {
|
|
||||||
channels: {
|
|
||||||
myMembers: {
|
|
||||||
[channelId]: {
|
|
||||||
channel_id: channelId,
|
|
||||||
last_viewed_at: 1000,
|
|
||||||
roles: '',
|
|
||||||
},
|
|
||||||
[prevChannelId]: {
|
|
||||||
channel_id: prevChannelId,
|
|
||||||
last_viewed_at: 1000,
|
|
||||||
roles: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
currentUserId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
nock(Client4.getBaseRoute()).
|
|
||||||
post('/channels/members/me/view', {channel_id: channelId, prev_channel_id: prevChannelId, collapsed_threads_supported: true}).
|
|
||||||
reply(200, OK_RESPONSE);
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
const result = await store.dispatch(Actions.viewChannel(channelId, prevChannelId));
|
|
||||||
expect(result).toEqual({data: true});
|
|
||||||
|
|
||||||
const state = store.getState();
|
|
||||||
expect(state.entities.channels.myMembers[channelId].last_viewed_at).toBeGreaterThan(now);
|
|
||||||
expect(state.entities.channels.myMembers[prevChannelId].last_viewed_at).toBeGreaterThan(now);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should clear manually unread state from current channel', async () => {
|
|
||||||
const channelId = TestHelper.generateId();
|
|
||||||
|
|
||||||
const currentUserId = TestHelper.generateId();
|
|
||||||
|
|
||||||
store = configureStore({
|
|
||||||
entities: {
|
|
||||||
channels: {
|
|
||||||
manuallyUnread: {
|
|
||||||
[channelId]: true,
|
|
||||||
},
|
|
||||||
myMembers: {
|
|
||||||
[channelId]: {
|
|
||||||
channel_id: channelId,
|
|
||||||
last_viewed_at: 1000,
|
|
||||||
roles: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
currentUserId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
nock(Client4.getBaseRoute()).
|
|
||||||
post('/channels/members/me/view', {channel_id: channelId, prev_channel_id: '', collapsed_threads_supported: true}).
|
|
||||||
reply(200, OK_RESPONSE);
|
|
||||||
|
|
||||||
const result = await store.dispatch(Actions.viewChannel(channelId));
|
|
||||||
expect(result).toEqual({data: true});
|
|
||||||
|
|
||||||
const state = store.getState();
|
|
||||||
expect(state.entities.channels.manuallyUnread[channelId]).not.toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should not update last_viewed_at of previous channel if it is manually marked as unread', async () => {
|
|
||||||
const channelId = TestHelper.generateId();
|
|
||||||
const prevChannelId = TestHelper.generateId();
|
|
||||||
|
|
||||||
const currentUserId = TestHelper.generateId();
|
|
||||||
|
|
||||||
store = configureStore({
|
|
||||||
entities: {
|
|
||||||
channels: {
|
|
||||||
manuallyUnread: {
|
|
||||||
[prevChannelId]: true,
|
|
||||||
},
|
|
||||||
myMembers: {
|
|
||||||
[channelId]: {
|
|
||||||
channel_id: channelId,
|
|
||||||
last_viewed_at: 1000,
|
|
||||||
roles: '',
|
|
||||||
},
|
|
||||||
[prevChannelId]: {
|
|
||||||
channel_id: prevChannelId,
|
|
||||||
last_viewed_at: 1000,
|
|
||||||
roles: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
currentUserId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
nock(Client4.getBaseRoute()).
|
|
||||||
post('/channels/members/me/view', {channel_id: channelId, prev_channel_id: '', collapsed_threads_supported: true}).
|
|
||||||
reply(200, OK_RESPONSE);
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
const result = await store.dispatch(Actions.viewChannel(channelId, prevChannelId));
|
|
||||||
expect(result).toEqual({data: true});
|
|
||||||
|
|
||||||
const state = store.getState();
|
|
||||||
expect(state.entities.channels.myMembers[channelId].last_viewed_at).toBeGreaterThan(now);
|
|
||||||
expect(state.entities.channels.myMembers[prevChannelId].last_viewed_at).toBe(1000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('markChannelAsViewed', async () => {
|
|
||||||
nock(Client4.getBaseRoute()).
|
|
||||||
post('/channels').
|
|
||||||
reply(201, TestHelper.fakeChannelWithId(TestHelper.basicTeam!.id));
|
|
||||||
|
|
||||||
const userChannel = await Client4.createChannel(
|
|
||||||
TestHelper.fakeChannel(TestHelper.basicTeam!.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
nock(Client4.getBaseRoute()).
|
|
||||||
get(`/users/me/teams/${TestHelper.basicTeam!.id}/channels`).
|
|
||||||
query(true).
|
|
||||||
reply(200, [userChannel, TestHelper.basicChannel]);
|
|
||||||
|
|
||||||
nock(Client4.getBaseRoute()).
|
|
||||||
get(`/users/me/teams/${TestHelper.basicTeam!.id}/channels/members`).
|
|
||||||
reply(200, [{user_id: TestHelper.basicUser!.id, roles: 'channel_user', channel_id: userChannel.id}, TestHelper.basicChannelMember]);
|
|
||||||
|
|
||||||
await store.dispatch(Actions.fetchMyChannelsAndMembersREST(TestHelper.basicTeam!.id));
|
|
||||||
|
|
||||||
const timestamp = Date.now();
|
|
||||||
let members = store.getState().entities.channels.myMembers;
|
|
||||||
let member = members[TestHelper.basicChannel!.id];
|
|
||||||
const otherMember = members[userChannel.id];
|
|
||||||
expect(member).toBeTruthy();
|
|
||||||
expect(otherMember).toBeTruthy();
|
|
||||||
|
|
||||||
await TestHelper.wait(50);
|
|
||||||
|
|
||||||
await store.dispatch(Actions.markChannelAsViewed(TestHelper.basicChannel!.id));
|
|
||||||
|
|
||||||
members = store.getState().entities.channels.myMembers;
|
|
||||||
member = members[TestHelper.basicChannel!.id];
|
|
||||||
expect(member.last_viewed_at > timestamp).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('markChannelAsUnread', () => {
|
|
||||||
it('plain message', () => {
|
|
||||||
const teamId = TestHelper.generateId();
|
|
||||||
const channelId = TestHelper.generateId();
|
|
||||||
const userId = TestHelper.generateId();
|
|
||||||
|
|
||||||
store = configureStore({
|
|
||||||
entities: {
|
|
||||||
channels: {
|
|
||||||
channels: {
|
|
||||||
[channelId]: {team_id: teamId},
|
|
||||||
},
|
|
||||||
messageCounts: {
|
|
||||||
[channelId]: {total: 10},
|
|
||||||
},
|
|
||||||
myMembers: {
|
|
||||||
[channelId]: {msg_count: 10, mention_count: 0},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
teams: {
|
|
||||||
myMembers: {
|
|
||||||
[teamId]: {msg_count: 0, mention_count: 0},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
currentUserId: userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
store.dispatch(Actions.markChannelAsUnread(teamId, channelId, [TestHelper.generateId()], false));
|
|
||||||
|
|
||||||
const state = store.getState();
|
|
||||||
expect(state.entities.channels.messageCounts[channelId].total).toEqual(11);
|
|
||||||
expect(state.entities.channels.myMembers[channelId].msg_count).toEqual(10);
|
|
||||||
expect(state.entities.channels.myMembers[channelId].mention_count).toEqual(0);
|
|
||||||
expect(state.entities.teams.myMembers[teamId].msg_count).toEqual(1);
|
|
||||||
expect(state.entities.teams.myMembers[teamId].mention_count).toEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('message mentioning current user', () => {
|
|
||||||
const teamId = TestHelper.generateId();
|
|
||||||
const channelId = TestHelper.generateId();
|
|
||||||
const userId = TestHelper.generateId();
|
|
||||||
|
|
||||||
store = configureStore({
|
|
||||||
entities: {
|
|
||||||
channels: {
|
|
||||||
channels: {
|
|
||||||
[channelId]: {team_id: teamId},
|
|
||||||
},
|
|
||||||
messageCounts: {
|
|
||||||
[channelId]: {total: 10},
|
|
||||||
},
|
|
||||||
myMembers: {
|
|
||||||
[channelId]: {msg_count: 10, mention_count: 0},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
teams: {
|
|
||||||
myMembers: {
|
|
||||||
[teamId]: {msg_count: 0, mention_count: 0},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
currentUserId: userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
store.dispatch(Actions.markChannelAsUnread(teamId, channelId, [userId], false));
|
|
||||||
|
|
||||||
const state = store.getState();
|
|
||||||
expect(state.entities.channels.messageCounts[channelId].total).toEqual(11);
|
|
||||||
expect(state.entities.channels.myMembers[channelId].msg_count).toEqual(10);
|
|
||||||
expect(state.entities.channels.myMembers[channelId].mention_count).toEqual(1);
|
|
||||||
expect(state.entities.teams.myMembers[teamId].msg_count).toEqual(1);
|
|
||||||
expect(state.entities.teams.myMembers[teamId].mention_count).toEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('plain message with mark_unread="mention"', () => {
|
|
||||||
const teamId = TestHelper.generateId();
|
|
||||||
const channelId = TestHelper.generateId();
|
|
||||||
const userId = TestHelper.generateId();
|
|
||||||
|
|
||||||
store = configureStore({
|
|
||||||
entities: {
|
|
||||||
channels: {
|
|
||||||
channels: {
|
|
||||||
[channelId]: {team_id: teamId},
|
|
||||||
},
|
|
||||||
messageCounts: {
|
|
||||||
[channelId]: {total: 10},
|
|
||||||
},
|
|
||||||
myMembers: {
|
|
||||||
[channelId]: {msg_count: 10, mention_count: 0, notify_props: {mark_unread: MarkUnread.MENTION}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
teams: {
|
|
||||||
myMembers: {
|
|
||||||
[teamId]: {msg_count: 0, mention_count: 0},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
currentUserId: userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
store.dispatch(Actions.markChannelAsUnread(teamId, channelId, [TestHelper.generateId()], false));
|
|
||||||
|
|
||||||
const state = store.getState();
|
|
||||||
expect(state.entities.channels.messageCounts[channelId].total).toEqual(11);
|
|
||||||
expect(state.entities.channels.myMembers[channelId].msg_count).toEqual(11);
|
|
||||||
expect(state.entities.channels.myMembers[channelId].mention_count).toEqual(0);
|
|
||||||
expect(state.entities.teams.myMembers[teamId].msg_count).toEqual(0);
|
|
||||||
expect(state.entities.teams.myMembers[teamId].mention_count).toEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('message mentioning current user with mark_unread="mention"', () => {
|
|
||||||
const teamId = TestHelper.generateId();
|
|
||||||
const channelId = TestHelper.generateId();
|
|
||||||
const userId = TestHelper.generateId();
|
|
||||||
|
|
||||||
store = configureStore({
|
|
||||||
entities: {
|
|
||||||
channels: {
|
|
||||||
channels: {
|
|
||||||
[channelId]: {team_id: teamId},
|
|
||||||
},
|
|
||||||
messageCounts: {
|
|
||||||
[channelId]: {total: 10},
|
|
||||||
},
|
|
||||||
myMembers: {
|
|
||||||
[channelId]: {msg_count: 10, mention_count: 0, notify_props: {mark_unread: MarkUnread.MENTION}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
teams: {
|
|
||||||
myMembers: {
|
|
||||||
[teamId]: {msg_count: 0, mention_count: 0},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
currentUserId: userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
store.dispatch(Actions.markChannelAsUnread(teamId, channelId, [userId], false));
|
|
||||||
|
|
||||||
const state = store.getState();
|
|
||||||
expect(state.entities.channels.messageCounts[channelId].total).toEqual(11);
|
|
||||||
expect(state.entities.channels.myMembers[channelId].msg_count).toEqual(11);
|
|
||||||
expect(state.entities.channels.myMembers[channelId].mention_count).toEqual(1);
|
|
||||||
expect(state.entities.teams.myMembers[teamId].msg_count).toEqual(0);
|
|
||||||
expect(state.entities.teams.myMembers[teamId].mention_count).toEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('channel member should not be updated if it has already been fetched', () => {
|
|
||||||
const teamId = TestHelper.generateId();
|
|
||||||
const channelId = TestHelper.generateId();
|
|
||||||
const userId = TestHelper.generateId();
|
|
||||||
|
|
||||||
store = configureStore({
|
|
||||||
entities: {
|
|
||||||
channels: {
|
|
||||||
channels: {
|
|
||||||
[channelId]: {team_id: teamId},
|
|
||||||
},
|
|
||||||
messageCounts: {
|
|
||||||
[channelId]: {total: 8},
|
|
||||||
},
|
|
||||||
myMembers: {
|
|
||||||
[channelId]: {msg_count: 5, mention_count: 2},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
teams: {
|
|
||||||
myMembers: {
|
|
||||||
[teamId]: {msg_count: 2, mention_count: 1},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
users: {
|
|
||||||
currentUserId: userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
store.dispatch(Actions.markChannelAsUnread(teamId, channelId, [userId], true));
|
|
||||||
|
|
||||||
const state = store.getState();
|
|
||||||
expect(state.entities.channels.messageCounts[channelId].total).toEqual(8);
|
|
||||||
expect(state.entities.channels.myMembers[channelId].msg_count).toEqual(5);
|
|
||||||
expect(state.entities.channels.myMembers[channelId].mention_count).toEqual(2);
|
|
||||||
expect(state.entities.teams.myMembers[teamId].msg_count).toEqual(3);
|
|
||||||
expect(state.entities.teams.myMembers[teamId].mention_count).toEqual(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('markChannelAsRead', () => {
|
describe('markChannelAsRead', () => {
|
||||||
it('one read channel', async () => {
|
it('one read channel', async () => {
|
||||||
const channelId = TestHelper.generateId();
|
const channelId = TestHelper.generateId();
|
||||||
@@ -1199,136 +841,6 @@ describe('Actions.Channels', () => {
|
|||||||
expect(state.entities.teams.myMembers[teamId].msg_count).toBe(3);
|
expect(state.entities.teams.myMembers[teamId].msg_count).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('two unread channels, same team, reading both', async () => {
|
|
||||||
const channelId1 = TestHelper.generateId();
|
|
||||||
const channelId2 = TestHelper.generateId();
|
|
||||||
const teamId = TestHelper.generateId();
|
|
||||||
|
|
||||||
store = configureStore({
|
|
||||||
entities: {
|
|
||||||
channels: {
|
|
||||||
channels: {
|
|
||||||
[channelId1]: {
|
|
||||||
id: channelId1,
|
|
||||||
team_id: teamId,
|
|
||||||
},
|
|
||||||
[channelId2]: {
|
|
||||||
id: channelId2,
|
|
||||||
team_id: teamId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
messageCounts: {
|
|
||||||
[channelId1]: {total: 10},
|
|
||||||
[channelId2]: {total: 12},
|
|
||||||
},
|
|
||||||
myMembers: {
|
|
||||||
[channelId1]: {
|
|
||||||
channel_id: channelId1,
|
|
||||||
mention_count: 2,
|
|
||||||
msg_count: 5,
|
|
||||||
last_viewed_at: 1000,
|
|
||||||
},
|
|
||||||
[channelId2]: {
|
|
||||||
channel_id: channelId2,
|
|
||||||
mention_count: 4,
|
|
||||||
msg_count: 9,
|
|
||||||
last_viewed_at: 2000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
teams: {
|
|
||||||
myMembers: {
|
|
||||||
[teamId]: {
|
|
||||||
id: teamId,
|
|
||||||
mention_count: 6,
|
|
||||||
msg_count: 8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await store.dispatch(Actions.markChannelAsRead(channelId1, channelId2));
|
|
||||||
|
|
||||||
const state = store.getState();
|
|
||||||
|
|
||||||
expect(state.entities.channels.myMembers[channelId1].mention_count).toBe(0);
|
|
||||||
expect(state.entities.channels.myMembers[channelId1].msg_count).toBe(state.entities.channels.messageCounts[channelId1].total);
|
|
||||||
expect(state.entities.channels.myMembers[channelId1].last_viewed_at).toBeGreaterThan(1000);
|
|
||||||
|
|
||||||
expect(state.entities.channels.myMembers[channelId2].mention_count).toBe(0);
|
|
||||||
expect(state.entities.channels.myMembers[channelId2].msg_count).toBe(state.entities.channels.messageCounts[channelId2].total);
|
|
||||||
expect(state.entities.channels.myMembers[channelId2].last_viewed_at).toBeGreaterThan(2000);
|
|
||||||
|
|
||||||
expect(state.entities.teams.myMembers[teamId].mention_count).toBe(0);
|
|
||||||
expect(state.entities.teams.myMembers[teamId].msg_count).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('two unread channels, same team, reading both (opposite order)', async () => {
|
|
||||||
const channelId1 = TestHelper.generateId();
|
|
||||||
const channelId2 = TestHelper.generateId();
|
|
||||||
const teamId = TestHelper.generateId();
|
|
||||||
|
|
||||||
store = configureStore({
|
|
||||||
entities: {
|
|
||||||
channels: {
|
|
||||||
channels: {
|
|
||||||
[channelId1]: {
|
|
||||||
id: channelId1,
|
|
||||||
team_id: teamId,
|
|
||||||
},
|
|
||||||
[channelId2]: {
|
|
||||||
id: channelId2,
|
|
||||||
team_id: teamId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
messageCounts: {
|
|
||||||
[channelId1]: {total: 10},
|
|
||||||
[channelId2]: {total: 12},
|
|
||||||
},
|
|
||||||
myMembers: {
|
|
||||||
[channelId1]: {
|
|
||||||
channel_id: channelId1,
|
|
||||||
mention_count: 2,
|
|
||||||
msg_count: 5,
|
|
||||||
last_viewed_at: 1000,
|
|
||||||
},
|
|
||||||
[channelId2]: {
|
|
||||||
channel_id: channelId2,
|
|
||||||
mention_count: 4,
|
|
||||||
msg_count: 9,
|
|
||||||
last_viewed_at: 1000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
teams: {
|
|
||||||
myMembers: {
|
|
||||||
[teamId]: {
|
|
||||||
id: teamId,
|
|
||||||
mention_count: 6,
|
|
||||||
msg_count: 8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await store.dispatch(Actions.markChannelAsRead(channelId2, channelId1));
|
|
||||||
|
|
||||||
const state = store.getState();
|
|
||||||
|
|
||||||
expect(state.entities.channels.myMembers[channelId1].mention_count).toBe(0);
|
|
||||||
expect(state.entities.channels.myMembers[channelId1].msg_count).toBe(state.entities.channels.messageCounts[channelId1].total);
|
|
||||||
expect(state.entities.channels.myMembers[channelId1].last_viewed_at).toBeGreaterThan(1000);
|
|
||||||
|
|
||||||
expect(state.entities.channels.myMembers[channelId2].mention_count).toBe(0);
|
|
||||||
expect(state.entities.channels.myMembers[channelId2].msg_count).toBe(state.entities.channels.messageCounts[channelId2].total);
|
|
||||||
expect(state.entities.channels.myMembers[channelId2].last_viewed_at).toBeGreaterThan(2000);
|
|
||||||
|
|
||||||
expect(state.entities.teams.myMembers[teamId].mention_count).toBe(0);
|
|
||||||
expect(state.entities.teams.myMembers[teamId].msg_count).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('two unread channels, different teams, reading one', async () => {
|
it('two unread channels, different teams, reading one', async () => {
|
||||||
const channelId1 = TestHelper.generateId();
|
const channelId1 = TestHelper.generateId();
|
||||||
const channelId2 = TestHelper.generateId();
|
const channelId2 = TestHelper.generateId();
|
||||||
@@ -1402,154 +914,6 @@ describe('Actions.Channels', () => {
|
|||||||
expect(state.entities.teams.myMembers[teamId2].mention_count).toBe(4);
|
expect(state.entities.teams.myMembers[teamId2].mention_count).toBe(4);
|
||||||
expect(state.entities.teams.myMembers[teamId2].msg_count).toBe(3);
|
expect(state.entities.teams.myMembers[teamId2].msg_count).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('two unread channels, different teams, reading both', async () => {
|
|
||||||
const channelId1 = TestHelper.generateId();
|
|
||||||
const channelId2 = TestHelper.generateId();
|
|
||||||
const teamId1 = TestHelper.generateId();
|
|
||||||
const teamId2 = TestHelper.generateId();
|
|
||||||
|
|
||||||
store = configureStore({
|
|
||||||
entities: {
|
|
||||||
channels: {
|
|
||||||
channels: {
|
|
||||||
[channelId1]: {
|
|
||||||
id: channelId1,
|
|
||||||
team_id: teamId1,
|
|
||||||
},
|
|
||||||
[channelId2]: {
|
|
||||||
id: channelId2,
|
|
||||||
team_id: teamId2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
messageCounts: {
|
|
||||||
[channelId1]: {total: 10},
|
|
||||||
[channelId2]: {total: 12},
|
|
||||||
},
|
|
||||||
myMembers: {
|
|
||||||
[channelId1]: {
|
|
||||||
channel_id: channelId1,
|
|
||||||
mention_count: 2,
|
|
||||||
msg_count: 5,
|
|
||||||
last_viewed_at: 1000,
|
|
||||||
},
|
|
||||||
[channelId2]: {
|
|
||||||
channel_id: channelId2,
|
|
||||||
mention_count: 4,
|
|
||||||
msg_count: 9,
|
|
||||||
last_viewed_at: 2000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
teams: {
|
|
||||||
myMembers: {
|
|
||||||
[teamId1]: {
|
|
||||||
id: teamId1,
|
|
||||||
mention_count: 2,
|
|
||||||
msg_count: 5,
|
|
||||||
},
|
|
||||||
[teamId2]: {
|
|
||||||
id: teamId2,
|
|
||||||
mention_count: 4,
|
|
||||||
msg_count: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await store.dispatch(Actions.markChannelAsRead(channelId1, channelId2));
|
|
||||||
|
|
||||||
const state = store.getState();
|
|
||||||
|
|
||||||
expect(state.entities.channels.myMembers[channelId1].mention_count).toBe(0);
|
|
||||||
expect(state.entities.channels.myMembers[channelId1].msg_count).toBe(state.entities.channels.messageCounts[channelId1].total);
|
|
||||||
expect(state.entities.channels.myMembers[channelId1].last_viewed_at).toBeGreaterThan(1000);
|
|
||||||
|
|
||||||
expect(state.entities.channels.myMembers[channelId2].mention_count).toBe(0);
|
|
||||||
expect(state.entities.channels.myMembers[channelId2].msg_count).toBe(state.entities.channels.messageCounts[channelId2].total);
|
|
||||||
expect(state.entities.channels.myMembers[channelId2].last_viewed_at).toBeGreaterThan(2000);
|
|
||||||
|
|
||||||
expect(state.entities.teams.myMembers[teamId1].mention_count).toBe(0);
|
|
||||||
expect(state.entities.teams.myMembers[teamId1].msg_count).toBe(0);
|
|
||||||
|
|
||||||
expect(state.entities.teams.myMembers[teamId2].mention_count).toBe(0);
|
|
||||||
expect(state.entities.teams.myMembers[teamId2].msg_count).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('two unread channels, different teams, reading both (opposite order)', async () => {
|
|
||||||
const channelId1 = TestHelper.generateId();
|
|
||||||
const channelId2 = TestHelper.generateId();
|
|
||||||
const teamId1 = TestHelper.generateId();
|
|
||||||
const teamId2 = TestHelper.generateId();
|
|
||||||
|
|
||||||
store = configureStore({
|
|
||||||
entities: {
|
|
||||||
channels: {
|
|
||||||
channels: {
|
|
||||||
[channelId1]: {
|
|
||||||
id: channelId1,
|
|
||||||
team_id: teamId1,
|
|
||||||
},
|
|
||||||
[channelId2]: {
|
|
||||||
id: channelId2,
|
|
||||||
team_id: teamId2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
messageCounts: {
|
|
||||||
[channelId1]: {total: 10},
|
|
||||||
[channelId2]: {total: 12},
|
|
||||||
},
|
|
||||||
myMembers: {
|
|
||||||
[channelId1]: {
|
|
||||||
channel_id: channelId1,
|
|
||||||
mention_count: 2,
|
|
||||||
msg_count: 5,
|
|
||||||
last_viewed_at: 1000,
|
|
||||||
},
|
|
||||||
[channelId2]: {
|
|
||||||
channel_id: channelId2,
|
|
||||||
mention_count: 4,
|
|
||||||
msg_count: 9,
|
|
||||||
last_viewed_at: 2000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
teams: {
|
|
||||||
myMembers: {
|
|
||||||
[teamId1]: {
|
|
||||||
id: teamId1,
|
|
||||||
mention_count: 2,
|
|
||||||
msg_count: 5,
|
|
||||||
},
|
|
||||||
[teamId2]: {
|
|
||||||
id: teamId2,
|
|
||||||
mention_count: 4,
|
|
||||||
msg_count: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await store.dispatch(Actions.markChannelAsRead(channelId1, channelId2));
|
|
||||||
|
|
||||||
const state = store.getState();
|
|
||||||
|
|
||||||
expect(state.entities.channels.myMembers[channelId1].mention_count).toBe(0);
|
|
||||||
expect(state.entities.channels.myMembers[channelId1].msg_count).toBe(state.entities.channels.messageCounts[channelId1].total);
|
|
||||||
expect(state.entities.channels.myMembers[channelId1].last_viewed_at).toBeGreaterThan(1000);
|
|
||||||
|
|
||||||
expect(state.entities.channels.myMembers[channelId2].mention_count).toBe(0);
|
|
||||||
expect(state.entities.channels.myMembers[channelId2].msg_count).toBe(state.entities.channels.messageCounts[channelId2].total);
|
|
||||||
expect(state.entities.channels.myMembers[channelId2].last_viewed_at).toBeGreaterThan(2000);
|
|
||||||
|
|
||||||
expect(state.entities.teams.myMembers[teamId1].mention_count).toBe(0);
|
|
||||||
expect(state.entities.teams.myMembers[teamId1].msg_count).toBe(0);
|
|
||||||
|
|
||||||
expect(state.entities.teams.myMembers[teamId2].mention_count).toBe(0);
|
|
||||||
expect(state.entities.teams.myMembers[teamId2].msg_count).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getChannels', async () => {
|
it('getChannels', async () => {
|
||||||
|
|||||||
@@ -728,102 +728,39 @@ export function unarchiveChannel(channelId: string): ActionFunc {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function viewChannel(channelId: string, prevChannelId = ''): ActionFunc {
|
export function updateApproximateViewTime(channelId: string): ActionFunc {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
const {currentUserId} = getState().entities.users;
|
const {currentUserId} = getState().entities.users;
|
||||||
|
|
||||||
const {myPreferences} = getState().entities.preferences;
|
const {myPreferences} = getState().entities.preferences;
|
||||||
|
|
||||||
const viewTimePref = myPreferences[`${Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME}--${channelId}`];
|
const viewTimePref = myPreferences[`${Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME}--${channelId}`];
|
||||||
const viewTime = viewTimePref ? parseInt(viewTimePref.value!, 10) : 0;
|
const viewTime = viewTimePref ? parseInt(viewTimePref.value!, 10) : 0;
|
||||||
const prevChanManuallyUnread = isManuallyUnread(getState(), prevChannelId);
|
|
||||||
|
|
||||||
if (viewTime < new Date().getTime() - (3 * 60 * 60 * 1000)) {
|
if (viewTime < new Date().getTime() - (3 * 60 * 60 * 1000)) {
|
||||||
const preferences = [
|
const preferences = [
|
||||||
{user_id: currentUserId, category: Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, name: channelId, value: new Date().getTime().toString()},
|
{user_id: currentUserId, category: Preferences.CATEGORY_CHANNEL_APPROXIMATE_VIEW_TIME, name: channelId, value: new Date().getTime().toString()},
|
||||||
];
|
];
|
||||||
savePreferences(currentUserId, preferences)(dispatch);
|
savePreferences(currentUserId, preferences)(dispatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
await Client4.viewMyChannel(channelId, prevChanManuallyUnread ? '' : prevChannelId);
|
|
||||||
} catch (error) {
|
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
|
||||||
dispatch(logError(error));
|
|
||||||
|
|
||||||
return {error};
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions: AnyAction[] = [];
|
|
||||||
|
|
||||||
const {myMembers} = getState().entities.channels;
|
|
||||||
const member = myMembers[channelId];
|
|
||||||
if (member) {
|
|
||||||
if (isManuallyUnread(getState(), channelId)) {
|
|
||||||
actions.push({
|
|
||||||
type: ChannelTypes.REMOVE_MANUALLY_UNREAD,
|
|
||||||
data: {channelId},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
actions.push({
|
|
||||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
|
|
||||||
data: {...member, last_viewed_at: new Date().getTime()},
|
|
||||||
});
|
|
||||||
dispatch(loadRolesIfNeeded(member.roles.split(' ')));
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevMember = myMembers[prevChannelId];
|
|
||||||
if (!prevChanManuallyUnread && prevMember) {
|
|
||||||
actions.push({
|
|
||||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
|
|
||||||
data: {...prevMember, last_viewed_at: new Date().getTime()},
|
|
||||||
});
|
|
||||||
dispatch(loadRolesIfNeeded(prevMember.roles.split(' ')));
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(batchActions(actions));
|
|
||||||
|
|
||||||
return {data: true};
|
return {data: true};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markChannelAsViewed(channelId: string, prevChannelId?: string): ActionFunc {
|
export function readMultipleChannels(channelIds: string[]): ActionFunc {
|
||||||
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
const actions = actionsToMarkChannelAsViewed(getState, channelId, prevChannelId);
|
let response;
|
||||||
|
try {
|
||||||
return dispatch(batchActions(actions));
|
response = await Client4.readMultipleChannels(channelIds);
|
||||||
};
|
} catch (error) {
|
||||||
}
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
|
dispatch(logError(error));
|
||||||
export function actionsToMarkChannelAsViewed(getState: GetStateFunc, channelId: string, prevChannelId = '') {
|
return {error};
|
||||||
const actions: AnyAction[] = [];
|
|
||||||
|
|
||||||
const state = getState();
|
|
||||||
const {myMembers} = state.entities.channels;
|
|
||||||
|
|
||||||
const member = myMembers[channelId];
|
|
||||||
if (member) {
|
|
||||||
actions.push({
|
|
||||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
|
|
||||||
data: {...member, last_viewed_at: Date.now()},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isManuallyUnread(state, channelId)) {
|
|
||||||
actions.push({
|
|
||||||
type: ChannelTypes.REMOVE_MANUALLY_UNREAD,
|
|
||||||
data: {channelId},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const prevMember = myMembers[prevChannelId];
|
dispatch(markMultipleChannelsAsRead(response.last_viewed_at_times));
|
||||||
if (prevMember && !isManuallyUnread(state, prevChannelId)) {
|
|
||||||
actions.push({
|
|
||||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
|
|
||||||
data: {...prevMember, last_viewed_at: Date.now()},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return actions;
|
return {data: true};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getChannels(teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE): ActionFunc {
|
export function getChannels(teamId: string, page = 0, perPage: number = General.CHANNELS_CHUNK_SIZE): ActionFunc {
|
||||||
@@ -1217,38 +1154,27 @@ export function updateChannelPurpose(channelId: string, purpose: string): Action
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markChannelAsRead(channelId: string, prevChannelId?: string, updateLastViewedAt = true): ActionFunc {
|
export function markChannelAsRead(channelId: string, skipUpdateViewTime = false): ActionFunc {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
const state = getState();
|
if (skipUpdateViewTime) {
|
||||||
const prevChanManuallyUnread = isManuallyUnread(state, prevChannelId);
|
dispatch(updateApproximateViewTime(channelId));
|
||||||
|
}
|
||||||
|
dispatch(markChannelAsViewedOnServer(channelId));
|
||||||
|
|
||||||
const actions = actionsToMarkChannelAsRead(getState, channelId, prevChannelId);
|
const actions = actionsToMarkChannelAsRead(getState, channelId);
|
||||||
|
if (actions.length > 0) {
|
||||||
|
dispatch(batchActions(actions));
|
||||||
|
}
|
||||||
|
|
||||||
// Send channel last viewed at to the server
|
return {data: true};
|
||||||
if (updateLastViewedAt) {
|
};
|
||||||
dispatch(markChannelAsViewedOnServer(channelId, prevChanManuallyUnread ? '' : prevChannelId));
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
export function markMultipleChannelsAsRead(channelTimes: Record<string, number>): ActionFunc {
|
||||||
|
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
// Don't use actionsToMarkChannelAsViewed here because that overwrites fields modified by
|
const actions: AnyAction[] = [];
|
||||||
// actionsToMarkChannelAsRead
|
for (const id of Object.keys(channelTimes)) {
|
||||||
actions.push({
|
actions.push(...actionsToMarkChannelAsRead(getState, id, channelTimes[id]));
|
||||||
type: ChannelTypes.RECEIVED_LAST_VIEWED_AT,
|
|
||||||
data: {
|
|
||||||
channel_id: channelId,
|
|
||||||
last_viewed_at: now,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (prevChannelId && !prevChanManuallyUnread) {
|
|
||||||
actions.push({
|
|
||||||
type: ChannelTypes.RECEIVED_LAST_VIEWED_AT,
|
|
||||||
data: {
|
|
||||||
channel_id: prevChannelId,
|
|
||||||
last_viewed_at: now,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actions.length > 0) {
|
if (actions.length > 0) {
|
||||||
@@ -1259,33 +1185,41 @@ export function markChannelAsRead(channelId: string, prevChannelId?: string, upd
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markChannelAsViewedOnServer(channelId: string, prevChannelId?: string): ActionFunc {
|
export function markChannelAsViewedOnServer(channelId: string): ActionFunc {
|
||||||
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
Client4.viewMyChannel(channelId, prevChannelId).then().catch((error) => {
|
try {
|
||||||
|
await Client4.viewMyChannel(channelId);
|
||||||
|
} catch (error) {
|
||||||
forceLogoutIfNecessary(error, dispatch, getState);
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
dispatch(logError(error));
|
dispatch(logError(error));
|
||||||
return {error};
|
return {error};
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// const actions: AnyAction[] = [];
|
||||||
|
// for (const id of Object.keys(response.last_viewed_at_times)) {
|
||||||
|
// actions.push({
|
||||||
|
// type: ChannelTypes.RECEIVED_LAST_VIEWED_AT,
|
||||||
|
// data: {
|
||||||
|
// channel_id: channelId,
|
||||||
|
// last_viewed_at: response.last_viewed_at_times[id],
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
return {data: true};
|
return {data: true};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function actionsToMarkChannelAsRead(getState: GetStateFunc, channelId: string, prevChannelId?: string) {
|
export function actionsToMarkChannelAsRead(getState: GetStateFunc, channelId: string, viewedAt = Date.now()) {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const {channels, messageCounts, myMembers} = state.entities.channels;
|
const {channels, messageCounts, myMembers} = state.entities.channels;
|
||||||
|
|
||||||
const prevChanManuallyUnread = isManuallyUnread(state, prevChannelId);
|
|
||||||
|
|
||||||
// Update channel member objects to set all mentions and posts as viewed
|
// Update channel member objects to set all mentions and posts as viewed
|
||||||
const channel = channels[channelId];
|
const channel = channels[channelId];
|
||||||
const messageCount = messageCounts[channelId];
|
const messageCount = messageCounts[channelId];
|
||||||
const prevChannel = (!prevChanManuallyUnread && prevChannelId) ? channels[prevChannelId] : null; // May be null since prevChannelId is optional
|
|
||||||
const prevMessageCount = (!prevChanManuallyUnread && prevChannelId) ? messageCounts[prevChannelId] : null;
|
|
||||||
|
|
||||||
// Update team member objects to set mentions and posts in channel as viewed
|
// Update team member objects to set mentions and posts in channel as viewed
|
||||||
const channelMember = myMembers[channelId];
|
const channelMember = myMembers[channelId];
|
||||||
const prevChannelMember = (!prevChanManuallyUnread && prevChannelId) ? myMembers[prevChannelId] : null; // May also be null
|
|
||||||
|
|
||||||
const actions: AnyAction[] = [];
|
const actions: AnyAction[] = [];
|
||||||
|
|
||||||
@@ -1310,6 +1244,14 @@ export function actionsToMarkChannelAsRead(getState: GetStateFunc, channelId: st
|
|||||||
amountUrgent: channelMember.urgent_mention_count,
|
amountUrgent: channelMember.urgent_mention_count,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
actions.push({
|
||||||
|
type: ChannelTypes.RECEIVED_LAST_VIEWED_AT,
|
||||||
|
data: {
|
||||||
|
channel_id: channelId,
|
||||||
|
last_viewed_at: viewedAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channel && isManuallyUnread(getState(), channelId)) {
|
if (channel && isManuallyUnread(getState(), channelId)) {
|
||||||
@@ -1319,82 +1261,9 @@ export function actionsToMarkChannelAsRead(getState: GetStateFunc, channelId: st
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevChannel && prevChannelMember && prevMessageCount) {
|
|
||||||
actions.push({
|
|
||||||
type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
|
|
||||||
data: {
|
|
||||||
teamId: prevChannel.team_id,
|
|
||||||
channelId: prevChannelId,
|
|
||||||
amount: prevMessageCount.total - prevChannelMember.msg_count,
|
|
||||||
amountRoot: prevMessageCount.root - prevChannelMember.msg_count_root,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
actions.push({
|
|
||||||
type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT,
|
|
||||||
data: {
|
|
||||||
teamId: prevChannel.team_id,
|
|
||||||
channelId: prevChannelId,
|
|
||||||
amount: prevChannelMember.mention_count,
|
|
||||||
amountRoot: prevChannelMember.mention_count_root,
|
|
||||||
amountUrgent: prevChannelMember.urgent_mention_count,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increments the number of posts in the channel by 1 and marks it as unread if necessary
|
|
||||||
export function markChannelAsUnread(teamId: string, channelId: string, mentions: string[], fetchedChannelMember = false, isRoot = false): ActionFunc {
|
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
|
||||||
const state = getState();
|
|
||||||
const {myMembers} = state.entities.channels;
|
|
||||||
const {currentUserId} = state.entities.users;
|
|
||||||
|
|
||||||
const actions: AnyAction[] = [{
|
|
||||||
type: ChannelTypes.INCREMENT_UNREAD_MSG_COUNT,
|
|
||||||
data: {
|
|
||||||
teamId,
|
|
||||||
channelId,
|
|
||||||
amount: 1,
|
|
||||||
amountRoot: isRoot ? 1 : 0,
|
|
||||||
onlyMentions: myMembers[channelId] && myMembers[channelId].notify_props &&
|
|
||||||
myMembers[channelId].notify_props.mark_unread === MarkUnread.MENTION,
|
|
||||||
fetchedChannelMember,
|
|
||||||
},
|
|
||||||
}];
|
|
||||||
|
|
||||||
if (!fetchedChannelMember) {
|
|
||||||
actions.push({
|
|
||||||
type: ChannelTypes.INCREMENT_TOTAL_MSG_COUNT,
|
|
||||||
data: {
|
|
||||||
channelId,
|
|
||||||
amountRoot: isRoot ? 1 : 0,
|
|
||||||
amount: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mentions && mentions.indexOf(currentUserId) !== -1) {
|
|
||||||
actions.push({
|
|
||||||
type: ChannelTypes.INCREMENT_UNREAD_MENTION_COUNT,
|
|
||||||
data: {
|
|
||||||
teamId,
|
|
||||||
channelId,
|
|
||||||
amountRoot: isRoot ? 1 : 0,
|
|
||||||
amount: 1,
|
|
||||||
fetchedChannelMember,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(batchActions(actions));
|
|
||||||
|
|
||||||
return {data: true};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function actionsToMarkChannelAsUnread(getState: GetStateFunc, teamId: string, channelId: string, mentions: string[], fetchedChannelMember = false, isRoot = false, priority = '') {
|
export function actionsToMarkChannelAsUnread(getState: GetStateFunc, teamId: string, channelId: string, mentions: string[], fetchedChannelMember = false, isRoot = false, priority = '') {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const {myMembers} = state.entities.channels;
|
const {myMembers} = state.entities.channels;
|
||||||
@@ -1609,8 +1478,6 @@ export default {
|
|||||||
joinChannel,
|
joinChannel,
|
||||||
deleteChannel,
|
deleteChannel,
|
||||||
unarchiveChannel,
|
unarchiveChannel,
|
||||||
viewChannel,
|
|
||||||
markChannelAsViewed,
|
|
||||||
getChannels,
|
getChannels,
|
||||||
autocompleteChannels,
|
autocompleteChannels,
|
||||||
autocompleteChannelsForSearch,
|
autocompleteChannelsForSearch,
|
||||||
@@ -1622,7 +1489,6 @@ export default {
|
|||||||
updateChannelHeader,
|
updateChannelHeader,
|
||||||
updateChannelPurpose,
|
updateChannelPurpose,
|
||||||
markChannelAsRead,
|
markChannelAsRead,
|
||||||
markChannelAsUnread,
|
|
||||||
favoriteChannel,
|
favoriteChannel,
|
||||||
unfavoriteChannel,
|
unfavoriteChannel,
|
||||||
membersMinusGroupMembers,
|
membersMinusGroupMembers,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const WebsocketEvents = {
|
|||||||
CHANNEL_DELETED: 'channel_deleted',
|
CHANNEL_DELETED: 'channel_deleted',
|
||||||
CHANNEL_UNARCHIVED: 'channel_restored',
|
CHANNEL_UNARCHIVED: 'channel_restored',
|
||||||
CHANNEL_UPDATED: 'channel_updated',
|
CHANNEL_UPDATED: 'channel_updated',
|
||||||
CHANNEL_VIEWED: 'channel_viewed',
|
MULTIPLE_CHANNELS_VIEWED: 'multiple_channels_viewed',
|
||||||
CHANNEL_MEMBER_UPDATED: 'channel_member_updated',
|
CHANNEL_MEMBER_UPDATED: 'channel_member_updated',
|
||||||
CHANNEL_SCHEME_UPDATED: 'channel_scheme_updated',
|
CHANNEL_SCHEME_UPDATED: 'channel_scheme_updated',
|
||||||
DIRECT_ADDED: 'direct_added',
|
DIRECT_ADDED: 'direct_added',
|
||||||
|
|||||||
@@ -208,6 +208,33 @@ export function makeGetFilteredChannelIdsForCategory(): (state: GlobalState, cat
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns a selector that, given a category, returns the ids of channels visible in that category. The returned channels do not
|
||||||
|
// include unread channels when the Unreads category is enabled.
|
||||||
|
export function makeGetUnreadIdsForCategory(): (state: GlobalState, category: ChannelCategory) => string[] {
|
||||||
|
const getChannelIdsForCategory = makeGetChannelIdsForCategory();
|
||||||
|
const emptyList: string[] = [];
|
||||||
|
|
||||||
|
return createSelector(
|
||||||
|
'makeGetFilteredChannelIdsForCategory',
|
||||||
|
getChannelIdsForCategory,
|
||||||
|
getUnreadChannelIdsSet,
|
||||||
|
shouldShowUnreadsCategory,
|
||||||
|
(channelIds, unreadChannelIdsSet, showUnreadsCategory) => {
|
||||||
|
if (showUnreadsCategory) {
|
||||||
|
return emptyList;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = channelIds.filter((id) => unreadChannelIdsSet.has(id));
|
||||||
|
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
return emptyList;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered.length === channelIds.length ? channelIds : filtered;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function getDraggingState(state: GlobalState): DraggingState {
|
export function getDraggingState(state: GlobalState): DraggingState {
|
||||||
return state.views.channelSidebar.draggingState;
|
return state.views.channelSidebar.draggingState;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -570,7 +570,7 @@ export const SocketEvents = {
|
|||||||
CHANNEL_DELETED: 'channel_deleted',
|
CHANNEL_DELETED: 'channel_deleted',
|
||||||
CHANNEL_UNARCHIVED: 'channel_restored',
|
CHANNEL_UNARCHIVED: 'channel_restored',
|
||||||
CHANNEL_UPDATED: 'channel_updated',
|
CHANNEL_UPDATED: 'channel_updated',
|
||||||
CHANNEL_VIEWED: 'channel_viewed',
|
MULTIPLE_CHANNELS_VIEWED: 'multiple_channels_viewed',
|
||||||
CHANNEL_MEMBER_UPDATED: 'channel_member_updated',
|
CHANNEL_MEMBER_UPDATED: 'channel_member_updated',
|
||||||
CHANNEL_SCHEME_UPDATED: 'channel_scheme_updated',
|
CHANNEL_SCHEME_UPDATED: 'channel_scheme_updated',
|
||||||
DIRECT_ADDED: 'direct_added',
|
DIRECT_ADDED: 'direct_added',
|
||||||
|
|||||||
@@ -1775,14 +1775,21 @@ export default class Client4 {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
viewMyChannel = (channelId: string, prevChannelId?: string) => {
|
viewMyChannel = (channelId: string) => {
|
||||||
const data = {channel_id: channelId, prev_channel_id: prevChannelId, collapsed_threads_supported: true};
|
const data = {channel_id: channelId, collapsed_threads_supported: true};
|
||||||
return this.doFetch<ChannelViewResponse>(
|
return this.doFetch<ChannelViewResponse>(
|
||||||
`${this.getChannelsRoute()}/members/me/view`,
|
`${this.getChannelsRoute()}/members/me/view`,
|
||||||
{method: 'post', body: JSON.stringify(data)},
|
{method: 'post', body: JSON.stringify(data)},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
readMultipleChannels = (channelIds: string[]) => {
|
||||||
|
return this.doFetch<ChannelViewResponse>(
|
||||||
|
`${this.getChannelsRoute()}/members/me/mark_read`,
|
||||||
|
{method: 'post', body: JSON.stringify(channelIds)},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
autocompleteChannels = (teamId: string, name: string) => {
|
autocompleteChannels = (teamId: string, name: string) => {
|
||||||
return this.doFetch<Channel[]>(
|
return this.doFetch<Channel[]>(
|
||||||
`${this.getTeamRoute(teamId)}/channels/autocomplete${buildQueryString({name})}`,
|
`${this.getTeamRoute(teamId)}/channels/autocomplete${buildQueryString({name})}`,
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user