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", 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]+}/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("/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) {
|
||||
c.RequireChannelId().RequireUserId()
|
||||
if c.Err != nil {
|
||||
|
||||
@@ -2478,9 +2478,10 @@ func TestViewChannel(t *testing.T) {
|
||||
CheckBadRequestStatus(t, resp)
|
||||
|
||||
view.ChannelId = "correctlysizedjunkdddfdfdf"
|
||||
_, resp, err = client.ViewChannel(context.Background(), th.BasicUser.Id, view)
|
||||
require.Error(t, err)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
viewResult, _, err := client.ViewChannel(context.Background(), th.BasicUser.Id, view)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, viewResult.LastViewedAtTimes, 0)
|
||||
|
||||
view.ChannelId = th.BasicChannel.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) {
|
||||
// I start looking for channels with notifications before I mark it as read, to clear the push notifications if needed
|
||||
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
|
||||
}
|
||||
var err error
|
||||
|
||||
member, err := a.Srv().Store().Channel().GetMember(context.Background(), channelID, userID)
|
||||
if err != nil {
|
||||
c.Logger().Warn("Failed to get membership", mlog.Err(err))
|
||||
continue
|
||||
}
|
||||
|
||||
notify := member.NotifyProps[model.PushNotifyProp]
|
||||
if notify == model.ChannelNotifyDefault {
|
||||
user, err := a.GetUser(userID)
|
||||
if err != nil {
|
||||
c.Logger().Warn("Failed to get user", mlog.String("user_id", userID), mlog.Err(err))
|
||||
continue
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
user, err := a.Srv().Store().User().Get(c.Context(), userID)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("MarkChannelsAsViewed", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
// We use channelsToView to later only update those, or early return if no channel is to be read
|
||||
channelsToView, channelsToClearPushNotifications, times, err := a.Srv().Store().Channel().GetChannelsWithUnreadsAndWithMentions(c.Context(), channelIDs, userID, user.NotifyProps)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("MarkChannelsAsViewed", "app.channel.get_channels_with_unreads_and_with_mentions.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
if len(channelsToView) == 0 {
|
||||
return times, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
updateThreads := *a.Config().ServiceSettings.ThreadAutoFollow && (!collapsedThreadsSupported || !isCRTEnabled)
|
||||
if updateThreads {
|
||||
err = a.Srv().Store().Thread().MarkAllAsReadByChannels(userID, channelIDs)
|
||||
err = a.Srv().Store().Thread().MarkAllAsReadByChannels(userID, channelsToView)
|
||||
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 {
|
||||
var invErr *store.ErrInvalidInput
|
||||
switch {
|
||||
@@ -3032,19 +3007,18 @@ func (a *App) MarkChannelsAsViewed(c request.CTX, channelIDs []string, userID st
|
||||
}
|
||||
|
||||
if *a.Config().ServiceSettings.EnableChannelViewedMessages {
|
||||
for _, channelID := range channelIDs {
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventChannelViewed, "", "", userID, nil, "")
|
||||
message.Add("channel_id", channelID)
|
||||
a.Publish(message)
|
||||
}
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventMultipleChannelsViewed, "", "", userID, nil, "")
|
||||
message.Add("channel_times", times)
|
||||
a.Publish(message)
|
||||
}
|
||||
|
||||
for _, channelID := range channelsToClearPushNotifications {
|
||||
a.clearPushNotification(currentSessionId, userID, channelID, "")
|
||||
}
|
||||
|
||||
if updateThreads && isCRTEnabled {
|
||||
timestamp := model.GetMillis()
|
||||
for _, channelID := range channelIDs {
|
||||
for _, channelID := range channelsToView {
|
||||
message := model.NewWebSocketEvent(model.WebsocketEventThreadReadChanged, "", channelID, userID, nil, "")
|
||||
message.Add("timestamp", timestamp)
|
||||
a.Publish(message)
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -18,7 +17,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -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) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
@@ -726,7 +726,7 @@ func (wc *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool {
|
||||
switch msg.EventType() {
|
||||
case model.WebsocketEventTyping,
|
||||
model.WebsocketEventStatusChange,
|
||||
model.WebsocketEventChannelViewed:
|
||||
model.WebsocketEventMultipleChannelsViewed:
|
||||
if time.Since(wc.lastLogTimeSlow) > websocketSuppressWarnThreshold {
|
||||
mlog.Warn(
|
||||
"websocket.slow: dropping message",
|
||||
|
||||
@@ -1113,26 +1113,6 @@ func TestCreatePostAsUser(t *testing.T) {
|
||||
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) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
@@ -1322,6 +1322,24 @@ func (s *OpenTracingLayerChannelStore) GetChannelsWithTeamDataByIds(channelIds [
|
||||
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) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
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) {
|
||||
|
||||
tries := 0
|
||||
|
||||
@@ -2020,6 +2020,82 @@ func (s SqlChannelStore) GetChannelMembersTimezones(channelId string) ([]model.S
|
||||
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) {
|
||||
selectSQL, args, err := s.channelMembersForTeamWithSchemeSelectQuery.
|
||||
Where(sq.Eq{
|
||||
@@ -2512,6 +2588,10 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string)
|
||||
TotalMsgCountRoot int64
|
||||
}{}
|
||||
|
||||
if len(channelIds) == 0 {
|
||||
return map[string]int64{}, nil
|
||||
}
|
||||
|
||||
// We use the question placeholder format for both databases, because
|
||||
// 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.
|
||||
|
||||
@@ -265,6 +265,7 @@ type ChannelStore interface {
|
||||
GetMembersInfoByChannelIds(channelIDs []string) (map[string][]*model.User, error)
|
||||
AnalyticsDeletedTypeCount(teamID string, channelType model.ChannelType) (int64, 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()
|
||||
ClearMembersForUserCache()
|
||||
GetChannelsByScheme(schemeID string, offset int, limit int) (model.ChannelList, error)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"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("SetShared", func(t *testing.T) { testSetShared(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) {
|
||||
@@ -8065,3 +8067,166 @@ func testGetTeamForChannel(t *testing.T, ss store.Store) {
|
||||
var nfErr *store.ErrNotFound
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
func (_m *ChannelStore) GetDeleted(team_id string, offset int, limit int, userID string) (model.ChannelList, error) {
|
||||
ret := _m.Called(team_id, offset, limit, userID)
|
||||
|
||||
@@ -1232,6 +1232,22 @@ func (s *TimerLayerChannelStore) GetChannelsWithTeamDataByIds(channelIds []strin
|
||||
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) {
|
||||
start := time.Now()
|
||||
|
||||
|
||||
@@ -4783,6 +4783,10 @@
|
||||
"id": "app.channel.get_channels_member_count.find.app_error",
|
||||
"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",
|
||||
"translation": "Unable to find the existing deleted channel."
|
||||
@@ -6743,6 +6747,10 @@
|
||||
"id": "app.terms_of_service.get.no_rows.app_error",
|
||||
"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",
|
||||
"translation": "update error"
|
||||
|
||||
@@ -3540,6 +3540,27 @@ func (c *Client4) ViewChannel(ctx context.Context, userId string, view *ChannelV
|
||||
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
|
||||
// unread messages and mentions for a user.
|
||||
func (c *Client4) GetChannelUnread(ctx context.Context, channelId, userId string) (*ChannelUnread, *Response, error) {
|
||||
|
||||
@@ -48,6 +48,7 @@ const (
|
||||
WebsocketEventResponse = "response"
|
||||
WebsocketEventEmojiAdded = "emoji_added"
|
||||
WebsocketEventChannelViewed = "channel_viewed"
|
||||
WebsocketEventMultipleChannelsViewed = "multiple_channels_viewed"
|
||||
WebsocketEventPluginStatusesChanged = "plugin_statuses_changed"
|
||||
WebsocketEventPluginEnabled = "plugin_enabled"
|
||||
WebsocketEventPluginDisabled = "plugin_disabled"
|
||||
|
||||
Ссылка в новой задаче
Block a user