* 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>
Этот коммит содержится в:
Daniel Espino García
2023-08-14 10:01:02 +02:00
коммит произвёл GitHub
родитель c1c07ba1bb
Коммит e9b3afecc2
60 изменённых файлов: 1151 добавлений и 1557 удалений

Просмотреть файл

@@ -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()