diff --git a/Makefile b/Makefile index 0fe01db270..6ccc076dde 100644 --- a/Makefile +++ b/Makefile @@ -149,12 +149,12 @@ TEMPLATES_DIR=templates PLUGIN_PACKAGES ?= mattermost-plugin-antivirus-v0.1.2 PLUGIN_PACKAGES += mattermost-plugin-autolink-v1.2.2 PLUGIN_PACKAGES += mattermost-plugin-aws-SNS-v1.2.0 -PLUGIN_PACKAGES += mattermost-plugin-calls-v0.7.1 +PLUGIN_PACKAGES += mattermost-plugin-calls-v0.8.1 PLUGIN_PACKAGES += mattermost-plugin-channel-export-v1.0.0 PLUGIN_PACKAGES += mattermost-plugin-custom-attributes-v1.3.0 PLUGIN_PACKAGES += mattermost-plugin-github-v2.0.1 PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.3.0 -PLUGIN_PACKAGES += mattermost-plugin-playbooks-v1.32.1 +PLUGIN_PACKAGES += mattermost-plugin-playbooks-v1.32.2 PLUGIN_PACKAGES += mattermost-plugin-jenkins-v1.1.0 PLUGIN_PACKAGES += mattermost-plugin-jira-v2.4.0 PLUGIN_PACKAGES += mattermost-plugin-nps-v1.2.0 diff --git a/api4/insights_test.go b/api4/insights_test.go index b0d8993779..1eca69c72c 100644 --- a/api4/insights_test.go +++ b/api4/insights_test.go @@ -821,25 +821,68 @@ func TestGetTopInactiveChannelsForTeamSince(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - // delete offtopic channel - which interferes with 'least' active channel results + // delete offtopic, town-square, th.basicchannel channel - which interferes with 'least' active channel results offTopicChannel, appErr := th.App.GetChannelByName(th.Context, "off-topic", th.BasicTeam.Id, false) require.Nil(t, appErr, "Expected nil, didn't receive nil") appErr = th.App.PermanentDeleteChannel(th.Context, offTopicChannel) require.Nil(t, appErr) + townSquareChannel, appErr := th.App.GetChannelByName(th.Context, "town-square", th.BasicTeam.Id, false) + require.Nil(t, appErr, "Expected nil, didn't receive nil") + appErr = th.App.PermanentDeleteChannel(th.Context, townSquareChannel) + require.Nil(t, appErr) + basicChannel, appErr := th.App.GetChannel(th.Context, th.BasicChannel.Id) + require.Nil(t, appErr, "Expected nil, didn't receive nil") + appErr = th.App.PermanentDeleteChannel(th.Context, basicChannel) + require.Nil(t, appErr) + basicChannel2, appErr := th.App.GetChannel(th.Context, th.BasicChannel2.Id) + require.Nil(t, appErr, "Expected nil, didn't receive nil") + appErr = th.App.PermanentDeleteChannel(th.Context, basicChannel2) + require.Nil(t, appErr) + basicPrivateChannel, appErr := th.App.GetChannel(th.Context, th.BasicPrivateChannel.Id) + require.Nil(t, appErr, "Expected nil, didn't receive nil") + appErr = th.App.PermanentDeleteChannel(th.Context, basicPrivateChannel) + require.Nil(t, appErr) th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional)) client := th.Client userId := th.BasicUser.Id - channel4 := th.CreatePublicChannel() - channel5 := th.CreatePrivateChannel() - channel6 := th.CreatePrivateChannel() + channel4Req := &model.Channel{ + DisplayName: "channel4", + Name: GenerateTestChannelName(), + Type: model.ChannelTypeOpen, + TeamId: th.BasicTeam.Id, + CreateAt: 1, + } + channel4, _, err := client.CreateChannel(channel4Req) + require.NoError(t, err) + + channel5Req := &model.Channel{ + DisplayName: "channel4", + Name: GenerateTestChannelName(), + Type: model.ChannelTypePrivate, + TeamId: th.BasicTeam.Id, + CreateAt: 1, + } + channel5, _, err := client.CreateChannel(channel5Req) + require.NoError(t, err) + + channel6Req := &model.Channel{ + DisplayName: "channel4", + Name: GenerateTestChannelName(), + Type: model.ChannelTypePrivate, + TeamId: th.BasicTeam.Id, + CreateAt: 1, + } + channel6, _, err := client.CreateChannel(channel6Req) + require.NoError(t, err) + th.App.AddUserToChannel(th.Context, th.BasicUser, channel4, false) th.App.AddUserToChannel(th.Context, th.BasicUser, channel5, false) th.App.AddUserToChannel(th.Context, th.BasicUser, channel6, false) - channelIDs := [6]string{th.BasicChannel.Id, th.BasicChannel2.Id, th.BasicPrivateChannel.Id, channel4.Id, channel5.Id, channel6.Id} + channelIDs := [3]string{channel4.Id, channel5.Id, channel6.Id} i := len(channelIDs) for _, channelID := range channelIDs { @@ -859,22 +902,19 @@ func TestGetTopInactiveChannelsForTeamSince(t *testing.T) { ID: channel6.Id, MessageCount: 1}, {ID: channel5.Id, MessageCount: 2}, {ID: channel4.Id, MessageCount: 3}, - {ID: th.BasicPrivateChannel.Id, MessageCount: 4}, - {ID: th.BasicChannel2.Id, MessageCount: 5}, - {ID: th.BasicChannel.Id, MessageCount: 7}, } t.Run("get-top-inactive-channels-for-team-since", func(t *testing.T) { - topInactiveChannels, _, err := client.GetTopInactiveChannelsForTeamSince(teamId, model.TimeRangeToday, 0, 5) + topInactiveChannels, _, err := client.GetTopInactiveChannelsForTeamSince(teamId, model.TimeRangeToday, 0, 2) require.NoError(t, err) for i, channel := range topInactiveChannels.Items { assert.Equal(t, expectedTopChannels[i].ID, channel.ID) } - topInactiveChannels, _, err = client.GetTopInactiveChannelsForTeamSince(teamId, model.TimeRangeToday, 1, 5) + topInactiveChannels, _, err = client.GetTopInactiveChannelsForTeamSince(teamId, model.TimeRangeToday, 1, 2) require.NoError(t, err) - assert.Equal(t, th.BasicChannel.Id, topInactiveChannels.Items[0].ID) + assert.Equal(t, channel4.Id, topInactiveChannels.Items[0].ID) }) t.Run("get-top-channels-for-user-since exclude channels user is not member of", func(t *testing.T) { @@ -887,7 +927,7 @@ func TestGetTopInactiveChannelsForTeamSince(t *testing.T) { th.RemoveUserFromChannel(th.BasicUser, excludedChannel) - topInactiveChannels, _, err := client.GetTopInactiveChannelsForUserSince(teamId, model.TimeRangeToday, 0, 5) + topInactiveChannels, _, err := client.GetTopInactiveChannelsForUserSince(teamId, model.TimeRangeToday, 0, 3) require.NoError(t, err) for i, channel := range topInactiveChannels.Items { @@ -897,7 +937,6 @@ func TestGetTopInactiveChannelsForTeamSince(t *testing.T) { } func TestGetTopDMsForUserSince(t *testing.T) { - t.Skip("MM-46911") th := Setup(t).InitBasic() defer th.TearDown() @@ -933,6 +972,7 @@ func TestGetTopDMsForUserSince(t *testing.T) { Username: GenerateTestUsername(), DisplayName: "a bot", Description: "bot", + UserId: model.NewId(), } createdBot, resp, err := th.Client.CreateBot(bot) diff --git a/app/channel_test.go b/app/channel_test.go index c4525ae706..93ce63dfa9 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -2685,13 +2685,30 @@ func TestGetTopInactiveChannelsForTeamSince(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - channel2 := th.CreateChannel(th.Context, th.BasicTeam) + channel2 := th.CreateChannel(th.Context, th.BasicTeam, WithCreateAt(1)) + channel3 := th.CreateChannel(th.Context, th.BasicTeam, WithCreateAt(1)) + channel4 := th.CreatePrivateChannel(th.Context, th.BasicTeam, WithCreateAt(1)) + channel5 := th.CreateChannel(th.Context, th.BasicTeam, WithCreateAt(1)) + channel6 := th.CreatePrivateChannel(th.Context, th.BasicTeam, WithCreateAt(1)) + th.AddUserToChannel(th.BasicUser, channel2) + th.AddUserToChannel(th.BasicUser, channel3) + th.AddUserToChannel(th.BasicUser, channel4) + th.AddUserToChannel(th.BasicUser, channel5) + th.AddUserToChannel(th.BasicUser, channel6) - // delete offtopic channel - which interferes with 'least' active channel results + // delete offtopic, town square, basicChannel channel - which interferes with 'least' active channel results offTopicChannel, appErr := th.App.GetChannelByName(th.Context, "off-topic", th.BasicTeam.Id, false) require.Nil(t, appErr, "Expected nil, didn't receive nil") appErr = th.App.PermanentDeleteChannel(th.Context, offTopicChannel) require.Nil(t, appErr) + townSquareChannel, appErr := th.App.GetChannelByName(th.Context, "town-square", th.BasicTeam.Id, false) + require.Nil(t, appErr, "Expected nil, didn't receive nil") + appErr = th.App.PermanentDeleteChannel(th.Context, townSquareChannel) + require.Nil(t, appErr) + basicChannel, appErr := th.App.GetChannel(th.Context, th.BasicChannel.Id) + require.Nil(t, appErr, "Expected nil, didn't receive nil") + appErr = th.App.PermanentDeleteChannel(th.Context, basicChannel) + require.Nil(t, appErr) // add a bot post to ensure it's counted _, err := th.Server.Store.Post().Save(&model.Post{ @@ -2704,8 +2721,6 @@ func TestGetTopInactiveChannelsForTeamSince(t *testing.T) { }) require.NoError(t, err) - channel3 := th.CreatePrivateChannel(th.Context, th.BasicTeam) - // add a webhook post to ensure it's counted _, err = th.Server.Store.Post().Save(&model.Post{ Message: "hello from a webhook", @@ -2717,16 +2732,7 @@ func TestGetTopInactiveChannelsForTeamSince(t *testing.T) { }) require.NoError(t, err) - channel4 := th.CreatePrivateChannel(th.Context, th.BasicTeam) - channel5 := th.CreateChannel(th.Context, th.BasicTeam) - channel6 := th.CreatePrivateChannel(th.Context, th.BasicTeam) - th.AddUserToChannel(th.BasicUser, channel2) - th.AddUserToChannel(th.BasicUser, channel3) - th.AddUserToChannel(th.BasicUser, channel4) - th.AddUserToChannel(th.BasicUser, channel5) - th.AddUserToChannel(th.BasicUser, channel6) - - channels := [6]*model.Channel{th.BasicChannel, channel2, channel3, channel4, channel5, channel6} + channels := [5]*model.Channel{channel2, channel3, channel4, channel5, channel6} i := len(channels) for _, channel := range channels { @@ -2745,13 +2751,12 @@ func TestGetTopInactiveChannelsForTeamSince(t *testing.T) { {ID: channel4.Id, MessageCount: 3}, {ID: channel3.Id, MessageCount: 5}, {ID: channel2.Id, MessageCount: 6}, - {ID: th.BasicChannel.Id, MessageCount: 7}, } timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location()) t.Run("get-top-channels-for-team-since", func(t *testing.T) { - topChannels, err := th.App.GetTopInactiveChannelsForTeamSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 6}) + topChannels, err := th.App.GetTopInactiveChannelsForTeamSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5}) require.Nil(t, err) for i, channel := range topChannels.Items { @@ -2759,10 +2764,16 @@ func TestGetTopInactiveChannelsForTeamSince(t *testing.T) { assert.Equal(t, expectedTopChannels[i].MessageCount, channel.MessageCount) } - topChannels, err = th.App.GetTopInactiveChannelsForTeamSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 1, PerPage: 5}) + topChannels, err = th.App.GetTopInactiveChannelsForTeamSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 1, PerPage: 4}) require.Nil(t, err) - assert.Equal(t, th.BasicChannel.Id, topChannels.Items[0].ID) - assert.Equal(t, int64(7), topChannels.Items[0].MessageCount) + assert.Equal(t, channel2.Id, topChannels.Items[0].ID) + assert.Equal(t, int64(6), topChannels.Items[0].MessageCount) + + // it simulates channel being created recently + _ = th.CreatePrivateChannel(th.Context, th.BasicTeam) + topChannels, err = th.App.GetTopInactiveChannelsForTeamSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 6}) + require.Nil(t, err) + assert.Equal(t, 5, len(topChannels.Items)) }) } @@ -2770,13 +2781,21 @@ func TestGetTopInactiveChannelsForUserSince(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() - // delete offtopic channel - which interferes with 'least' active channel results + // delete offtopic, town-square, th.basicchannel channels - which interferes with 'least' active channel results offTopicChannel, appErr := th.App.GetChannelByName(th.Context, "off-topic", th.BasicTeam.Id, false) require.Nil(t, appErr, "Expected nil, didn't receive nil") appErr = th.App.PermanentDeleteChannel(th.Context, offTopicChannel) require.Nil(t, appErr) + townSquareChannel, appErr := th.App.GetChannelByName(th.Context, "town-square", th.BasicTeam.Id, false) + require.Nil(t, appErr, "Expected nil, didn't receive nil") + appErr = th.App.PermanentDeleteChannel(th.Context, townSquareChannel) + require.Nil(t, appErr) + basicChannel, appErr := th.App.GetChannel(th.Context, th.BasicChannel.Id) + require.Nil(t, appErr, "Expected nil, didn't receive nil") + appErr = th.App.PermanentDeleteChannel(th.Context, basicChannel) + require.Nil(t, appErr) - channel2 := th.CreateChannel(th.Context, th.BasicTeam) + channel2 := th.CreateChannel(th.Context, th.BasicTeam, WithCreateAt(1)) // add a bot post to ensure it's counted _, err := th.Server.Store.Post().Save(&model.Post{ @@ -2789,7 +2808,7 @@ func TestGetTopInactiveChannelsForUserSince(t *testing.T) { }) require.NoError(t, err) - channel3 := th.CreatePrivateChannel(th.Context, th.BasicTeam) + channel3 := th.CreatePrivateChannel(th.Context, th.BasicTeam, WithCreateAt(1)) // add a webhook post to ensure it's counted _, err = th.Server.Store.Post().Save(&model.Post{ @@ -2802,16 +2821,16 @@ func TestGetTopInactiveChannelsForUserSince(t *testing.T) { }) require.NoError(t, err) - channel4 := th.CreatePrivateChannel(th.Context, th.BasicTeam) - channel5 := th.CreateChannel(th.Context, th.BasicTeam) - channel6 := th.CreatePrivateChannel(th.Context, th.BasicTeam) + channel4 := th.CreatePrivateChannel(th.Context, th.BasicTeam, WithCreateAt(1)) + channel5 := th.CreateChannel(th.Context, th.BasicTeam, WithCreateAt(1)) + channel6 := th.CreatePrivateChannel(th.Context, th.BasicTeam, WithCreateAt(1)) th.AddUserToChannel(th.BasicUser, channel2) th.AddUserToChannel(th.BasicUser, channel3) th.AddUserToChannel(th.BasicUser, channel4) th.AddUserToChannel(th.BasicUser, channel5) th.AddUserToChannel(th.BasicUser, channel6) - channels := [6]*model.Channel{th.BasicChannel, channel2, channel3, channel4, channel5, channel6} + channels := [5]*model.Channel{channel2, channel3, channel4, channel5, channel6} i := len(channels) for _, channel := range channels { @@ -2830,24 +2849,23 @@ func TestGetTopInactiveChannelsForUserSince(t *testing.T) { {ID: channel4.Id, MessageCount: 3}, {ID: channel3.Id, MessageCount: 5}, {ID: channel2.Id, MessageCount: 6}, - {ID: th.BasicChannel.Id, MessageCount: 7}, } timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location()) t.Run("get-top-channels-for-user-since", func(t *testing.T) { - topChannels, err := th.App.GetTopInactiveChannelsForUserSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5}) + topChannels, err := th.App.GetTopInactiveChannelsForUserSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 4}) require.Nil(t, err) - require.Equal(t, len(topChannels.Items), 5) + require.Equal(t, len(topChannels.Items), 4) for i, channel := range topChannels.Items { assert.Equal(t, expectedTopChannels[i].ID, channel.ID) assert.Equal(t, expectedTopChannels[i].MessageCount, channel.MessageCount) } - topChannels, err = th.App.GetTopInactiveChannelsForUserSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 1, PerPage: 5}) + topChannels, err = th.App.GetTopInactiveChannelsForUserSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 1, PerPage: 4}) require.Nil(t, err) require.Equal(t, len(topChannels.Items), 1) - assert.Equal(t, th.BasicChannel.Id, topChannels.Items[0].ID) - assert.Equal(t, int64(7), topChannels.Items[0].MessageCount) + assert.Equal(t, channel2.Id, topChannels.Items[0].ID) + assert.Equal(t, int64(6), topChannels.Items[0].MessageCount) }) } diff --git a/app/helper_test.go b/app/helper_test.go index 5b15d3f519..7de517ea58 100644 --- a/app/helper_test.go +++ b/app/helper_test.go @@ -327,12 +327,18 @@ func WithShared(v bool) ChannelOption { } } +func WithCreateAt(v int64) ChannelOption { + return func(channel *model.Channel) { + channel.CreateAt = *model.NewInt64(v) + } +} + func (th *TestHelper) CreateChannel(c request.CTX, team *model.Team, options ...ChannelOption) *model.Channel { return th.createChannel(c, team, model.ChannelTypeOpen, options...) } -func (th *TestHelper) CreatePrivateChannel(c request.CTX, team *model.Team) *model.Channel { - return th.createChannel(c, team, model.ChannelTypePrivate) +func (th *TestHelper) CreatePrivateChannel(c request.CTX, team *model.Team, options ...ChannelOption) *model.Channel { + return th.createChannel(c, team, model.ChannelTypePrivate, options...) } func (th *TestHelper) createChannel(c request.CTX, team *model.Team, channelType model.ChannelType, options ...ChannelOption) *model.Channel { diff --git a/app/notification_push.go b/app/notification_push.go index 757764c613..a50b16ceab 100644 --- a/app/notification_push.go +++ b/app/notification_push.go @@ -219,30 +219,42 @@ func (a *App) getPushNotificationMessage(contentsConfig, postMessage string, exp return senderName + userLocale("api.post.send_notifications_and_forget.push_general_message") } +func (a *App) getUserBadgeCount(userID string, isCRTEnabled bool) (int, *model.AppError) { + unreadCount, err := a.Srv().Store.User().GetUnreadCount(userID, isCRTEnabled) + if err != nil { + return 0, model.NewAppError("getUserBadgeCount", "app.user.get_unread_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + badgeCount := int(unreadCount) + + if isCRTEnabled { + threadUnreadMentions, err := a.Srv().Store.Thread().GetTotalUnreadMentions(userID, "", model.GetUserThreadsOpts{}) + if err != nil { + return 0, model.NewAppError("getUserBadgeCount", "app.user.get_thread_count_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + badgeCount += int(threadUnreadMentions) + } + + return badgeCount, nil +} + func (a *App) clearPushNotificationSync(c request.CTX, currentSessionId, userID, channelID, rootID string) *model.AppError { + isCRTEnabled := a.IsCRTEnabledForUser(c, userID) + + badgeCount, err := a.getUserBadgeCount(userID, isCRTEnabled) + if err != nil { + return model.NewAppError("clearPushNotificationSync", "app.user.get_badge_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + msg := &model.PushNotification{ Type: model.PushTypeClear, Version: model.PushMessageV2, ChannelId: channelID, RootId: rootID, ContentAvailable: 1, - Badge: 0, - IsCRTEnabled: a.IsCRTEnabledForUser(c, userID), + Badge: badgeCount, + IsCRTEnabled: isCRTEnabled, } - unreadCount, err := a.Srv().Store.User().GetUnreadCount(userID) - if err != nil { - return model.NewAppError("clearPushNotificationSync", "app.user.get_unread_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err) - } - msg.Badge = int(unreadCount) - - if msg.IsCRTEnabled { - totalUnreadMentions, err := a.Srv().Store.Thread().GetTotalUnreadMentions(userID, "", model.GetUserThreadsOpts{}) - if err != nil { - return model.NewAppError("clearPushNotificationSync", "app.user.get_thread_count_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err) - } - msg.Badge += int(totalUnreadMentions) - } return a.sendPushNotificationToAllSessions(msg, userID, currentSessionId) } @@ -260,21 +272,19 @@ func (a *App) clearPushNotification(currentSessionId, userID, channelID, rootID } } -func (a *App) updateMobileAppBadgeSync(userID string) *model.AppError { +func (a *App) updateMobileAppBadgeSync(c request.CTX, userID string) *model.AppError { + badgeCount, err := a.getUserBadgeCount(userID, a.IsCRTEnabledForUser(c, userID)) + if err != nil { + return model.NewAppError("updateMobileAppBadgeSync", "app.user.get_badge_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + } + msg := &model.PushNotification{ Type: model.PushTypeUpdateBadge, Version: model.PushMessageV2, Sound: "none", ContentAvailable: 1, + Badge: badgeCount, } - - unreadCount, err := a.Srv().Store.User().GetUnreadCount(userID) - if err != nil { - return model.NewAppError("updateMobileAppBadgeSync", "app.user.get_unread_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err) - } - - msg.Badge = int(unreadCount) - return a.sendPushNotificationToAllSessions(msg, userID, "") } @@ -345,7 +355,7 @@ func (hub *PushNotificationsHub) start(c request.CTX) { notification.replyToThreadType, ) case notificationTypeUpdateBadge: - err = hub.app.updateMobileAppBadgeSync(notification.userID) + err = hub.app.updateMobileAppBadgeSync(c, notification.userID) default: mlog.Debug("Invalid notification type", mlog.String("notification_type", string(notification.notificationType))) } @@ -566,11 +576,12 @@ func (a *App) BuildPushNotificationMessage(c request.CTX, contentsConfig string, msg = a.buildFullPushNotificationMessage(c, contentsConfig, post, user, channel, channelName, senderName, explicitMention, channelWideMention, replyToThreadType) } - unreadCount, err := a.Srv().Store.User().GetUnreadCount(user.Id) + badgeCount, err := a.getUserBadgeCount(user.Id, a.IsCRTEnabledForUser(c, user.Id)) if err != nil { - return nil, model.NewAppError("BuildPushNotificationMessage", "app.user.get_unread_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err) + return nil, model.NewAppError("BuildPushNotificationMessage", "app.user.get_badge_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } - msg.Badge = int(unreadCount) + + msg.Badge = badgeCount return msg, nil } diff --git a/app/notification_push_test.go b/app/notification_push_test.go index 0f3e8a157f..dfb226573e 100644 --- a/app/notification_push_test.go +++ b/app/notification_push_test.go @@ -1136,7 +1136,7 @@ func TestClearPushNotificationSync(t *testing.T) { mockStore := th.App.Srv().Store.(*mocks.Store) mockUserStore := mocks.UserStore{} mockUserStore.On("Count", mock.Anything).Return(int64(10), nil) - mockUserStore.On("GetUnreadCount", mock.AnythingOfType("string")).Return(int64(1), nil) + mockUserStore.On("GetUnreadCount", mock.AnythingOfType("string"), mock.AnythingOfType("bool")).Return(int64(1), nil) mockPostStore := mocks.PostStore{} mockPostStore.On("GetMaxPostSize").Return(65535, nil) mockSystemStore := mocks.SystemStore{} @@ -1212,7 +1212,7 @@ func TestUpdateMobileAppBadgeSync(t *testing.T) { mockStore := th.App.Srv().Store.(*mocks.Store) mockUserStore := mocks.UserStore{} mockUserStore.On("Count", mock.Anything).Return(int64(10), nil) - mockUserStore.On("GetUnreadCount", mock.AnythingOfType("string")).Return(int64(1), nil) + mockUserStore.On("GetUnreadCount", mock.AnythingOfType("string"), mock.AnythingOfType("bool")).Return(int64(1), nil) mockPostStore := mocks.PostStore{} mockPostStore.On("GetMaxPostSize").Return(65535, nil) mockSystemStore := mocks.SystemStore{} @@ -1231,9 +1231,10 @@ func TestUpdateMobileAppBadgeSync(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.EmailSettings.PushNotificationServer = pushServer.URL + *cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDisabled }) - err := th.App.updateMobileAppBadgeSync("user1") + err := th.App.updateMobileAppBadgeSync(th.Context, "user1") require.Nil(t, err) // Server side verification. // We verify that 2 requests have been sent, and also check the message contents. @@ -1529,7 +1530,7 @@ func BenchmarkPushNotificationThroughput(b *testing.B) { mockStore := th.App.Srv().Store.(*mocks.Store) mockUserStore := mocks.UserStore{} mockUserStore.On("Count", mock.Anything).Return(int64(10), nil) - mockUserStore.On("GetUnreadCount", mock.AnythingOfType("string")).Return(int64(1), nil) + mockUserStore.On("GetUnreadCount", mock.AnythingOfType("string"), mock.AnythingOfType("bool")).Return(int64(1), nil) mockPostStore := mocks.PostStore{} mockPostStore.On("GetMaxPostSize").Return(65535, nil) mockSystemStore := mocks.SystemStore{} diff --git a/app/web_hub_test.go b/app/web_hub_test.go index f4f347cb7f..e53a2b51d0 100644 --- a/app/web_hub_test.go +++ b/app/web_hub_test.go @@ -135,7 +135,7 @@ func TestHubSessionRevokeRace(t *testing.T) { mockUserStore := mocks.UserStore{} mockUserStore.On("Count", mock.Anything).Return(int64(10), nil) - mockUserStore.On("GetUnreadCount", mock.AnythingOfType("string")).Return(int64(1), nil) + mockUserStore.On("GetUnreadCount", mock.AnythingOfType("string"), mock.AnythingOfType("bool")).Return(int64(1), nil) mockPostStore := mocks.PostStore{} mockPostStore.On("GetMaxPostSize").Return(65535, nil) mockSystemStore := mocks.SystemStore{} diff --git a/build/Dockerfile b/build/Dockerfile index 9ad11d9272..90c15906c4 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -31,10 +31,10 @@ RUN apt-get update \ ucf=3.0038+nmu1 \ openssl=1.1.1n-0+deb10u3 \ libkeyutils1=1.6-6 \ - libkrb5support0=1.17-3+deb10u3 \ - libk5crypto3=1.17-3+deb10u3 \ - libkrb5-3=1.17-3+deb10u3 \ - libgssapi-krb5-2=1.17-3+deb10u3 \ + libkrb5support0=1.17-3+deb10u4 \ + libk5crypto3=1.17-3+deb10u4 \ + libkrb5-3=1.17-3+deb10u4 \ + libgssapi-krb5-2=1.17-3+deb10u4 \ libnghttp2-14=1.36.0-2+deb10u1 \ libpsl5=0.20.2-2 \ librtmp1=2.4+20151223.gitfa8646d.1-2 \ diff --git a/config/client.go b/config/client.go index c8e9eedc52..7315ab6898 100644 --- a/config/client.go +++ b/config/client.go @@ -131,6 +131,7 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li props["CollapsedThreads"] = *c.ServiceSettings.CollapsedThreads props["EnableCustomGroups"] = "false" props["InsightsEnabled"] = "false" + props["PostPriority"] = strconv.FormatBool(*c.ServiceSettings.PostPriority) if license != nil { props["ExperimentalEnableAuthenticationTransfer"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableAuthenticationTransfer) diff --git a/i18n/bg.json b/i18n/bg.json index e3d1ea8a2f..7913a2775f 100644 --- a/i18n/bg.json +++ b/i18n/bg.json @@ -4023,10 +4023,6 @@ "id": "api.templates.password_change_body.info", "translation": "Вашата парола е актуализирана за {{.TeamDisplayName}} на {{.TeamURL}} от {{.Method}}." }, - { - "id": "api.templates.over_limit_fix_now", - "translation": "Оправи сега" - }, { "id": "api.templates.mfa_deactivated_body.title", "translation": "Многофакторното удостоверяване бе премахнато" diff --git a/i18n/de.json b/i18n/de.json index 35d6d194d4..ae6d93035c 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -4569,7 +4569,7 @@ }, { "id": "oauth.gitlab.tos.error", - "translation": "Die Nutzungsbedingungen von GitLab haben sich geändert. Bitte gehe zu gitlab.com um sie zu akzeptieren und versuche dann, dich erneut an Mattermost anzumelden." + "translation": "Die Nutzungsbedingungen von GitLab haben sich geändert. Bitte gehe zu {{.URL}} um sie zu akzeptieren und versuche dann, dich erneut an Mattermost anzumelden." }, { "id": "plugin.api.update_user_status.bad_status", @@ -8008,15 +8008,15 @@ }, { "id": "api.templates.payment_failed.title", - "translation": "Fehlgeschlagene Zahlung" + "translation": "Die Zahlung war nicht erfolgreich" }, { "id": "api.templates.payment_failed.subject", - "translation": "Aktion notwendig: Zahlung für Mattermost Cloud fehlgeschlagen" + "translation": "Aktion notwendig: Zahlung für Mattermost {{.Plan}} fehlgeschlagen" }, { "id": "api.templates.payment_failed.info3", - "translation": "Um einen unterbrechungsfreien Betrieb deines Mattermost Cloud Abonnements zu gewährleisten, kontaktiere bitte dein Finanzinstitut um das Problem zu lösen oder aktualisiere deine Zahlungsinformationen. Sobald die Zahlungsinformationen aktualisiert wurden, wird Mattermost versuchen den Außenstand auszugleichen." + "translation": "Um einen unterbrechungsfreien Zugriff auf Mattermost {{.Plan}} zu gewährleisten, kontaktiere bitte dein Finanzinstitut um das Problem zu lösen oder aktualisiere deine Zahlungsinformationen. Sobald die Zahlungsinformationen aktualisiert wurden, wird Mattermost versuchen den Außenstand auszugleichen." }, { "id": "api.templates.payment_failed.info2", @@ -8026,10 +8026,6 @@ "id": "api.templates.payment_failed.info1", "translation": "Dein Finanzinstitut hat ein Zahlung mit deiner {{.CardBrand}} Kreditkarte mit der Nummer ****{{.LastFour}}, die für deinen Mattermost Cloud Arbeitsbereich hinterlegt ist, abgelehnt." }, - { - "id": "api.templates.over_limit_fix_now", - "translation": "Jetzt beheben" - }, { "id": "api.upload.upload_data.multipart_error", "translation": "Verarbeitung der Multipart-Daten fehlgeschlagen." @@ -9529,5 +9525,17 @@ { "id": "app.cloud.trial_plan_bot_message", "translation": "{{.UsersNum}} Mitglieder des {{.WorkspaceName}} Arbeitsbereichs haben den Start des Enterprise-Tests angefragt für Zugriff auf: " + }, + { + "id": "app.cloud.get_current_plan_name.app_error", + "translation": "Abrufen des aktuellen Plan Namens nicht möglich" + }, + { + "id": "ent.saml.configure.certificate_parse_error.app_error", + "translation": "SAML konnte das öffentliche Zertifikat des Identity Providers nicht erfolgreich laden. Bitte kontaktiere deinen Systemadmin." + }, + { + "id": "app.user.get_badge_count.app_error", + "translation": "Wir konnten den Nachrichtenzähler für den Benutzer nicht abfragen." } ] diff --git a/i18n/en.json b/i18n/en.json index abf4c2280a..aeeca11e0a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -6591,6 +6591,10 @@ "id": "app.user.get.app_error", "translation": "We encountered an error finding the account." }, + { + "id": "app.user.get_badge_count.app_error", + "translation": "We could not get the badge count for the user." + }, { "id": "app.user.get_by_auth.missing_account.app_error", "translation": "Unable to find an existing account matching your authentication type for this team. This team may require an invite from the team owner to join." @@ -7687,6 +7691,10 @@ "id": "ent.saml.build_request.encoding.app_error", "translation": "An error occurred while encoding the request for the Identity Provider. Please contact your System Administrator." }, + { + "id": "ent.saml.configure.certificate_parse_error.app_error", + "translation": "SAML could not load Identity Provider Public Certificate successfully, please contact your system administrator." + }, { "id": "ent.saml.configure.encryption_not_enabled.app_error", "translation": "SAML login was unsuccessful because encryption is not enabled. Please contact your System Administrator." diff --git a/i18n/en_AU.json b/i18n/en_AU.json index ce533a1723..eafc534942 100644 --- a/i18n/en_AU.json +++ b/i18n/en_AU.json @@ -5019,10 +5019,6 @@ "id": "api.templates.password_change_body.info", "translation": "Your password has been updated for {{.TeamDisplayName}} on {{ .TeamURL }} by {{.Method}}." }, - { - "id": "api.templates.over_limit_fix_now", - "translation": "Fix Now" - }, { "id": "ent.compliance.csv.metadata.export.appError", "translation": "Unable to add metadata file to the zip file." @@ -9505,5 +9501,29 @@ { "id": "api.cloud.delinquency_email.missing_email_to_trigger", "translation": "Missing required fields to send delinquency email." + }, + { + "id": "app.notify_admin.send_notification_post.app_error", + "translation": "Unable to send notification post." + }, + { + "id": "app.notify_admin.save.app_error", + "translation": "Unable to save notify data." + }, + { + "id": "app.cloud.upgrade_plan_bot_message_single", + "translation": "{{.UsersNum}} member of the {{.WorkspaceName}} workspace has requested a workspace upgrade for: " + }, + { + "id": "app.cloud.upgrade_plan_bot_message", + "translation": "{{.UsersNum}} members of the {{.WorkspaceName}} workspace have requested a workspace upgrade for: " + }, + { + "id": "app.cloud.trial_plan_bot_message_single", + "translation": "{{.UsersNum}} member of the {{.WorkspaceName}} workspace has requested starting the Enterprise trial for access to: " + }, + { + "id": "app.cloud.trial_plan_bot_message", + "translation": "{{.UsersNum}} members of the {{.WorkspaceName}} workspace have requested starting the Enterprise trial for access to: " } ] diff --git a/i18n/es.json b/i18n/es.json index c3ce264e1a..97cec02eba 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -7663,10 +7663,6 @@ "id": "api.templates.payment_failed.info1", "translation": "Su institución financiera rechazó un pago de su {{.CardBrand}} ****{{.LastFour}} asociado a su espacio de trabajo de Mattermost Cloud." }, - { - "id": "api.templates.over_limit_fix_now", - "translation": "Arreglar ahora" - }, { "id": "api.oauth.redirecting_back", "translation": "Redirigiéndote de nuevo a la aplicación." diff --git a/i18n/fa.json b/i18n/fa.json index 4bd2716514..b71235d64b 100644 --- a/i18n/fa.json +++ b/i18n/fa.json @@ -4895,10 +4895,6 @@ "id": "api.templates.password_change_body.info", "translation": "رمز ورود شما برای {{.TeamDisplayName}} در {{.TeamURL}} توسط {{.Method}} به روز شده است." }, - { - "id": "api.templates.over_limit_fix_now", - "translation": "اکنون رفع کنید" - }, { "id": "api.templates.mfa_deactivated_body.title", "translation": "احراز هویت چند عاملی حذف شد" diff --git a/i18n/fr.json b/i18n/fr.json index b173cc06bc..043cda6743 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -7927,10 +7927,6 @@ "id": "api.templates.payment_failed.info2", "translation": "La raison suivante a été fournie :" }, - { - "id": "api.templates.over_limit_fix_now", - "translation": "Résoudre maintenant" - }, { "id": "api.templates.license_up_for_renewal_title", "translation": "Votre abonnement à Mattermost doit être renouvelé" diff --git a/i18n/hu.json b/i18n/hu.json index 14893d7263..b10693e9f8 100644 --- a/i18n/hu.json +++ b/i18n/hu.json @@ -5499,10 +5499,6 @@ "id": "api.templates.password_change_body.info", "translation": "Jelszava frissítésre került a {{.TeamDisplayName}} ({{ .TeamURL }}) csapatban {{.Method}} által." }, - { - "id": "api.templates.over_limit_fix_now", - "translation": "Javítás most" - }, { "id": "api.templates.mfa_deactivated_body.title", "translation": "A többtényezős hitelesítést eltávolítottuk" diff --git a/i18n/it.json b/i18n/it.json index 020ceeea2e..2b4114bad9 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -9107,10 +9107,6 @@ "id": "api.templates.verify_body.info1", "translation": " " }, - { - "id": "api.templates.over_limit_fix_now", - "translation": " " - }, { "id": "api.templates.cloud_welcome_email.signin_sub_info", "translation": " " diff --git a/i18n/ja.json b/i18n/ja.json index 50b6de4219..743dfa5997 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -7723,10 +7723,6 @@ "id": "api.templates.payment_failed.info1", "translation": "Mattermost Cloudワークスペースに関連付けられたあなたの {{.CardBrand}} ****{{.LastFour}} からの支払いを金融機関が拒否しました。" }, - { - "id": "api.templates.over_limit_fix_now", - "translation": "今すぐ対応する" - }, { "id": "api.system.update_viewed_notices.failed", "translation": "閲覧済みのお知らせを更新できませんでした" diff --git a/i18n/nl.json b/i18n/nl.json index 42c9e5d8af..08e471d2cc 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -7671,10 +7671,6 @@ "id": "api.templates.payment_failed.info1", "translation": "Jouw financiële instelling weigerde een betaling van uw {{.CardBrand}} ****{{.LastFour}} geassocieerd met uw Mattermost Cloud werkruimte." }, - { - "id": "api.templates.over_limit_fix_now", - "translation": "Los het nu op" - }, { "id": "api.roles.patch_roles.not_allowed_permission.error", "translation": "Een of meer van de volgende rechten die je probeert toe te voegen of te verwijderen zijn niet toegestaan" @@ -9317,5 +9313,165 @@ { "id": "api.cloud.delinquency_email.missing_email_to_trigger", "translation": "Verplichte velden ontbreken voor het versturen van een e-mail over wanbetaling." + }, + { + "id": "api.templates.delinquency_7.title", + "translation": "Jouw betaling is niet voltooid" + }, + { + "id": "api.templates.delinquency_7.subtitle2", + "translation": "Om jouw {{.Plan}} plan actief te houden, dien je zo snel mogelijk contact op te nemen met jouw financiële instelling. Werk vervolgens jouw betalingsgegevens zo nodig bij." + }, + { + "id": "api.templates.delinquency_7.subtitle1", + "translation": "We konden jouw laatste betaling niet verwerken" + }, + { + "id": "api.templates.delinquency_7.button", + "translation": "Betalingsgegevens bijwerken" + }, + { + "id": "api.templates.delinquency_60.title", + "translation": "Jouw Mattermost-werkruimte wordt na 30 dagen gedowngraded" + }, + { + "id": "api.templates.delinquency_60.subtitle3", + "translation": "Werk nu jouw betalingsgegevens bij of downgrade naar Cloud Starter hieronder." + }, + { + "id": "api.templates.delinquency_60.subtitle2", + "translation": "Wij zullen jouw werkruimte automatisch downgraden na 30 dagen als wij niet in staat zijn om jouw betaling te verwerken." + }, + { + "id": "api.templates.delinquency_60.subtitle1", + "translation": "Gelieve jouw betalingsgegevens spoedig bij te werken om jouw openstaande facturen te verwerken." + }, + { + "id": "api.templates.delinquency_60.subject", + "translation": "Actie vereist: Werkruimte zal binnen 30 dagen gedowngraded worden" + }, + { + "id": "api.templates.delinquency_60.downgrade_to_starter", + "translation": "Downgraden naar Cloud Starter" + }, + { + "id": "api.templates.delinquency_60.button", + "translation": "Betalingsgegevens bijwerken" + }, + { + "id": "api.templates.delinquency_45.title", + "translation": "Jouw werkruimte zal binnenkort gedowngraded worden" + }, + { + "id": "api.templates.delinquency_45.subtitle3", + "translation": "Werk nu jouw creditcardgegevens bij." + }, + { + "id": "api.templates.delinquency_45.subtitle2", + "translation": "Een gedowngradede workspace kan een negatieve invloed hebben op kritische workflows, integraties en andere bedrijfskritische activiteiten die in jouw workspace worden uitgevoerd." + }, + { + "id": "api.templates.delinquency_45.subtitle1", + "translation": "We hebben geen betaling kunnen innen voor openstaande facturen van {{.DelinquencyDate}}. Jouw werkruimte loopt het risico om gedowngraded te worden." + }, + { + "id": "api.templates.delinquency_45.subject", + "translation": "Melding: Jouw Mattermost {{.Plan}} zal binnenkort gedowngraded worden" + }, + { + "id": "api.templates.delinquency_45.button", + "translation": "Betalingsgegevens bijwerken" + }, + { + "id": "api.templates.delinquency_30.title", + "translation": "Jouw werkruimte zal binnenkort gedowngraded worden" + }, + { + "id": "api.templates.delinquency_30.subtitle2", + "translation": "als geen actie wordt ondernomen, zal jouw werkruimte worden gedowngraded en kunnen de volgende gegevens worden gearchiveerd:" + }, + { + "id": "api.templates.delinquency_30.subtitle1", + "translation": "Je hebt tijd om jouw Mattermost {{.Plan}} actief te houden, maar je moet de problemen met jouw betalingsmethode oplossen." + }, + { + "id": "api.templates.delinquency_30.subject", + "translation": "Handel om jouw Mattermost {{.Plan}} eigenschappen te behouden" + }, + { + "id": "api.templates.delinquency_30.limits_documentation", + "translation": "Bekijk alle documentatie rond beperkingen." + }, + { + "id": "api.templates.delinquency_30.button", + "translation": "Betalingsgegevens bijwerken" + }, + { + "id": "api.templates.delinquency_30.bullet.plugins", + "translation": "Actieve plugins en integraties" + }, + { + "id": "api.templates.delinquency_30.bullet.message_history", + "translation": "Berichtengeschiedenis" + }, + { + "id": "api.templates.delinquency_30.bullet.files", + "translation": "Bestanden" + }, + { + "id": "api.templates.delinquency_30.bullet.cards", + "translation": "Kaarten van jouw Boards" + }, + { + "id": "api.templates.delinquency_14.title", + "translation": "Betaling niet ontvangen" + }, + { + "id": "api.templates.delinquency_14.subtitle2", + "translation": "Neem contact op met jouw financiële instelling om eventuele problemen op te lossen. Werk vervolgens jouw betalingsgegevens bij indien nodig." + }, + { + "id": "api.templates.delinquency_14.subtitle1", + "translation": "We waren niet in staat om de kredietkaart die we in ons bestand hebben in rekening te brengen. Dit betekent dat jouw werkruimte het risico loopt te worden gedegradeerd naar Cloud Starter." + }, + { + "id": "api.templates.delinquency_90.subject", + "translation": "Jouw Mattermost Cloud-werkruimte werd gedowngraded" + }, + { + "id": "api.templates.delinquency_90.secondary_action_button", + "translation": "Plannen en prijzen bekijken" + }, + { + "id": "api.templates.delinquency_90.button", + "translation": "Betalingsgegevens bijwerken" + }, + { + "id": "api.templates.delinquency_75.title", + "translation": "Over 15 dagen zal jouw werkruimte wordeng edowngrade" + }, + { + "id": "api.templates.delinquency_75.subtitle3", + "translation": "Werk nu jouw betalingsgegevens bij, of downgrade naar Cloud Starter." + }, + { + "id": "api.templates.delinquency_75.subtitle2", + "translation": "Jouw werkruimte zal worden gedowngrade naar Cloud Starter. Jouw {{.Plan}} functies zullen worden vergrendeld en sommige van jouw werkruimtegegevens kunnen worden gearchiveerd totdat je jouw volledige uitstaande saldo hebt voldaan." + }, + { + "id": "api.templates.delinquency_75.subtitle1", + "translation": "Dit is de laatste herinnering dat we geen betaling hebben ontvangen voor jouw Mattermost Cloud-werkruimte sinds {{.DelinquencyDate}}" + }, + { + "id": "api.templates.delinquency_75.subject", + "translation": "Jouw Mattermost {{.Plan}} zal over 15 dagen worden gedowngraded" + }, + { + "id": "api.templates.delinquency_75.downgrade_to_starter", + "translation": "Downgraden naar Cloud Starter" + }, + { + "id": "api.templates.delinquency_75.button", + "translation": "Betalingsgegevens bijwerken" } ] diff --git a/i18n/pl.json b/i18n/pl.json index a405acb796..a5593cfa3c 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -4573,7 +4573,7 @@ }, { "id": "oauth.gitlab.tos.error", - "translation": "Zaktualizowano Warunki usługi GitLab. Przejdź na stronę gitlab.com, aby je zaakceptować, a następnie spróbuj zalogować się ponownie do Mattermost." + "translation": "Warunki korzystania z usług GitLab zostały zaktualizowane. Proszę przejść do {{.URL}}, aby je zaakceptować, a następnie spróbować ponownie zalogować się do Mattermost." }, { "id": "plugin.api.update_user_status.bad_status", @@ -8149,15 +8149,15 @@ }, { "id": "api.templates.payment_failed.title", - "translation": "Nieudana płatność" + "translation": "Płatność nie powiodła się" }, { "id": "api.templates.payment_failed.subject", - "translation": "Wymagane działanie: Nieudana płatność za Mattermost Cloud" + "translation": "Wymagane działanie: Płatność nie powiodła się dla Mattermost {{.Plan}}" }, { "id": "api.templates.payment_failed.info3", - "translation": "Aby zapewnić nieprzerwaną subskrypcję Mattermost Cloud, należy skontaktować się ze swoją instytucją finansową w celu rozwiązania problemu lub zaktualizować informacje dotyczące płatności. Po zaktualizowaniu informacji o płatności, Mattermost podejmie próbę uregulowania wszelkich zaległości." + "translation": "Aby zapewnić nieprzerwany dostęp do Mattermost {{.Plan}}, należy skontaktować się ze swoją instytucją finansową w celu rozwiązania problemu lub zaktualizować informacje dotyczące płatności. Po zaktualizowaniu informacji o płatności, Mattermost podejmie próbę uregulowania wszelkich zaległości." }, { "id": "api.templates.payment_failed.info2", @@ -8167,10 +8167,6 @@ "id": "api.templates.payment_failed.info1", "translation": "Twoja instytucja finansowa odrzuciła płatność z Twojej {{.CardBrand}} ****{{.LastFour}} powiązanej z obszarem roboczym Mattermost Cloud." }, - { - "id": "api.templates.over_limit_fix_now", - "translation": "Napraw teraz" - }, { "id": "store.sql_file_info.search.disabled", "translation": "Wyszukiwanie plików zostało wyłączone na tym serwerze. Skontaktuj się z Administratorem Systemu." @@ -9530,5 +9526,17 @@ { "id": "app.cloud.trial_plan_bot_message", "translation": "{{.UsersNum}} członkowie obszaru roboczego {{.WorkspaceName}} zażądali rozpoczęcia wersji próbnej Enterprise w celu uzyskania dostępu do: " + }, + { + "id": "app.cloud.get_current_plan_name.app_error", + "translation": "Nie można uzyskać nazwy bieżącego planu" + }, + { + "id": "ent.saml.configure.certificate_parse_error.app_error", + "translation": "SAML nie mógł pomyślnie załadować Identity Provider Public Certificate, skontaktuj się z administratorem systemu." + }, + { + "id": "app.user.get_badge_count.app_error", + "translation": "Nie mogliśmy uzyskać liczby odznak dla użytkownika." } ] diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json index 97ba1d0bdb..94b78b3e58 100644 --- a/i18n/pt-BR.json +++ b/i18n/pt-BR.json @@ -7663,10 +7663,6 @@ "id": "api.templates.payment_failed.info1", "translation": "Sua instituição financeira recusou um pagamento de seu {{.CardBrand}} ****{{.LastFour}} associado ao seu espaço de trabalho Mattermost Cloud." }, - { - "id": "api.templates.over_limit_fix_now", - "translation": "Corrigir Agora" - }, { "id": "model.config.is_valid.import.retention_days_too_low.app_error", "translation": "Valor inválido para RetentionDays. O valor é muito baixo." diff --git a/i18n/ro.json b/i18n/ro.json index 999a2c9fc8..c51b4666e0 100644 --- a/i18n/ro.json +++ b/i18n/ro.json @@ -7283,10 +7283,6 @@ "id": "api.cloud.app_error", "translation": "Eroare internă la solicitarea api cloud." }, - { - "id": "api.templates.over_limit_fix_now", - "translation": "Repară acum" - }, { "id": "api.templates.email_us_anytime_at", "translation": "Trimiteți-ne un e-mail oricând la " diff --git a/i18n/ru.json b/i18n/ru.json index 20751ec396..3d371f6a36 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -7947,10 +7947,6 @@ "id": "api.templates.payment_failed.info1", "translation": "Ваше финансовое учреждение отклонило платёж вашей карты {{.CardBrand}}. ****{{.LastFour}}, связанный с вашим рабочим пространством Mattermost Cloud." }, - { - "id": "api.templates.over_limit_fix_now", - "translation": "Исправить сейчас" - }, { "id": "api.templates.invite_body_guest.subTitle", "translation": "Вы были приглашены в качестве гостя в команду" diff --git a/i18n/sv.json b/i18n/sv.json index b73d75fb5c..e3ede34fd8 100644 --- a/i18n/sv.json +++ b/i18n/sv.json @@ -241,7 +241,7 @@ }, { "id": "oauth.gitlab.tos.error", - "translation": "GitLab's användningsvillkor har uppdaterats. Logga in på gitlab.com för att acceptera dem och logga sedan in i Mattermost igen." + "translation": "GitLab's användningsvillkor har uppdaterats. Gå till {{.URL}} för att acceptera dem och logga sedan in i Mattermost igen." }, { "id": "model.group_syncable.syncable_id.app_error", @@ -4925,15 +4925,15 @@ }, { "id": "api.templates.payment_failed.title", - "translation": "Misslyckad betalning" + "translation": "Betalningen gick inte bra" }, { "id": "api.templates.payment_failed.subject", - "translation": "Åtgärd krävs: Betalning för Mattermost Cloud misslyckades" + "translation": "Åtgärd krävs: Betalning för Mattermost {{.Plan}} misslyckades" }, { "id": "api.templates.payment_failed.info3", - "translation": "För att säkerställa en fortsatt prenumeration på Mattermost Cloud bör du kontakta din kortutgivare för att åtgärda problemet, alternativt uppdatera dina betaluppgifter. När betalinformationen är uppdaterad kommer Mattermost försöka reglera eventuellt utestående saldo." + "translation": "För att säkerställa en fortsatt tillgång på Mattermost {{.Plan}} bör du kontakta din kortutgivare för att åtgärda problemet, alternativt uppdatera dina betaluppgifter. När betalinformationen är uppdaterad kommer Mattermost försöka reglera eventuellt utestående saldo." }, { "id": "api.templates.payment_failed.info2", @@ -4955,10 +4955,6 @@ "id": "api.templates.password_change_body.info", "translation": "Ditt lösenord har uppdaterats av {{.Method}} för {{.TeamDisplayName}} på {{ .TeamURL }}." }, - { - "id": "api.templates.over_limit_fix_now", - "translation": "Fixa nu" - }, { "id": "api.templates.mfa_deactivated_body.title", "translation": "Flerfaktorauthentisering är borttagen" @@ -9381,5 +9377,165 @@ { "id": "api.templates.delinquency_14.subject", "translation": "Betalningen för din Mattermost {{.Plan}} är försenad." + }, + { + "id": "ent.saml.configure.certificate_parse_error.app_error", + "translation": "SAML kunde inte ladda din Identity Providers Publika Certificat. Kontakta din systemadministratör." + }, + { + "id": "app.user.get_badge_count.app_error", + "translation": "Vi kunde inte få fram användarens antal märken." + }, + { + "id": "app.notify_admin.send_notification_post.app_error", + "translation": "Det går inte att skicka notifieringsmeddelande." + }, + { + "id": "app.notify_admin.save.app_error", + "translation": "Det går inte att spara uppgifter om notifiering." + }, + { + "id": "app.cloud.upgrade_plan_bot_message_single", + "translation": "{{.UsersNum}} medlemmar i arbetsytan {{.WorkspaceName}} har önskat en uppgradering för att få tillgång till: " + }, + { + "id": "app.cloud.upgrade_plan_bot_message", + "translation": "{{.UsersNum}} medlemmar i arbetsytan {{.WorkspaceName}} har önskat en uppgradering för att få tillgång till: " + }, + { + "id": "app.cloud.trial_plan_bot_message_single", + "translation": "{{.UsersNum}} medlemmar i arbetsytan {{.WorkspaceName}} har önskat starta Enterprise trial för att få tillgång till: " + }, + { + "id": "app.cloud.trial_plan_bot_message", + "translation": "{{.UsersNum}} medlemmar i arbetsytan {{.WorkspaceName}} har önskat starta Enterprise trial för att få tillgång till: " + }, + { + "id": "app.cloud.get_subscription_delinquency_date.app_error", + "translation": "Abonnemanget är inte försenat" + }, + { + "id": "app.cloud.get_subscription.app_error", + "translation": "Kunde inte hämta molnprenumeration" + }, + { + "id": "app.cloud.get_current_plan_name.app_error", + "translation": "Det går inte att hämta fram namnet på den aktuella planen" + }, + { + "id": "app.cloud.get_cloud_products.app_error", + "translation": "Kunde inte hämta molnprodukter" + }, + { + "id": "api.templates.delinquency_90.title", + "translation": "Din Mattermost-arbetsyta har nedgraderats" + }, + { + "id": "api.templates.delinquency_90.subtitle3", + "translation": "Uppdatera betalningsinformationen om du vill ta tillbaka ditt data från arkivet och behålla betal-funktioner." + }, + { + "id": "api.templates.delinquency_90.subtitle2", + "translation": "Dessutom kan dina data ha arkiverats på grund av begränsningar i Cloud Starter." + }, + { + "id": "api.templates.delinquency_90.subtitle1", + "translation": "Om du använder Cloud Professional- eller Enterprise-funktioner för viktiga affärsaktiviteter kommer dessa inte längre att vara tillgängliga och du kommer att uppleva försämrad prestanda." + }, + { + "id": "api.templates.delinquency_90.subject", + "translation": "Din Mattermost Cloud-arbetsyta har nedgraderats" + }, + { + "id": "api.templates.delinquency_90.secondary_action_button", + "translation": "Visa abonnemang och priser" + }, + { + "id": "api.templates.delinquency_90.button", + "translation": "Uppdatera betalningsinformation" + }, + { + "id": "api.templates.delinquency_75.title", + "translation": "Din arbetsyta kommer att nedgraderas om 15 dagar" + }, + { + "id": "api.templates.delinquency_75.subtitle3", + "translation": "Uppdatera din betalningsinformation nu, eller nedgradera till Cloud Starter." + }, + { + "id": "api.templates.delinquency_75.subtitle2", + "translation": "Din arbetsplats kommer att nedgraderas till Cloud Starter. Dina {{.Plan}}-funktioner kommer att spärras och delar av dina arbetsytedata kan komma att arkiveras tills hela ditt utestående belopp är betalt." + }, + { + "id": "api.templates.delinquency_75.subtitle1", + "translation": "Detta är en sista påminnelse. Vi har inte mottagit betalning för din Mattermost Cloud-arbetsyta sedan {{.DelinquencyDate}}" + }, + { + "id": "api.templates.delinquency_75.subject", + "translation": "Din Mattermost {{.Plan}} kommer att nedgraderas om 15 dagar" + }, + { + "id": "api.templates.delinquency_75.downgrade_to_starter", + "translation": "Nedgradera till Cloud Starter" + }, + { + "id": "api.templates.delinquency_75.button", + "translation": "Uppdatera betalningsinformation" + }, + { + "id": "api.templates.delinquency_7.title", + "translation": "Din betalning slutfördes inte" + }, + { + "id": "api.templates.delinquency_7.subtitle2", + "translation": "För att hålla din {{.Plan}}-plan aktiv, kontakta din bank eller kortutgivare så snart som möjligt. Uppdatera din betalningsinformation vid behov." + }, + { + "id": "api.templates.delinquency_7.subtitle1", + "translation": "Vi kunde inte behandla din senaste betalning" + }, + { + "id": "api.templates.delinquency_7.button", + "translation": "Uppdatera betalningsinformation" + }, + { + "id": "api.templates.delinquency_60.title", + "translation": "Din Mattermost-arbetsyta kommer att nedgraderas om 30 dagar" + }, + { + "id": "api.templates.delinquency_60.subtitle3", + "translation": "Uppdatera din betalningsinformation nu eller nedgradera till Cloud Starter nedan." + }, + { + "id": "api.templates.delinquency_60.subtitle2", + "translation": "Vi nedgraderar din arbetsyta automatiskt om 30 dagar om vi inte kan behandla din betalning." + }, + { + "id": "api.templates.delinquency_60.subtitle1", + "translation": "Uppdatera din betalningsinformation snarast så att utestående fakturor kan hanteras." + }, + { + "id": "api.templates.delinquency_60.subject", + "translation": "Åtgärder krävs: Arbetsytan kommer att nedgraderas inom 30 dagar" + }, + { + "id": "api.templates.delinquency_60.downgrade_to_starter", + "translation": "Nedgradera till Cloud Starter" + }, + { + "id": "api.templates.delinquency_60.button", + "translation": "Uppdatera betalningsinformation" + }, + { + "id": "api.templates.delinquency_45.title", + "translation": "Din arbetsyta kommer snart att nedgraderas" + }, + { + "id": "api.templates.delinquency_45.subtitle3", + "translation": "Uppdatera din kreditkortsinformation nu." + }, + { + "id": "api.cloud.delinquency_email.missing_email_to_trigger", + "translation": "Information i obligatoriska fält saknas för att kunna skicka e-postmeddelanden om utebliven betalning." } ] diff --git a/i18n/tr.json b/i18n/tr.json index a20d37adb2..05ef8c4e65 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -4569,7 +4569,7 @@ }, { "id": "oauth.gitlab.tos.error", - "translation": "GitLab hizmet koşulları güncellendi. Lütfen gitlab.com adresine giderek yeni hizmet koşullarını onayladıktan sonra yeniden Mattermost oturumu açmayı deneyin." + "translation": "GitLab hizmet koşulları güncellendi. Lütfen {{.URL}} adresine giderek yeni hizmet koşullarını onayladıktan sonra Mattermost oturumunu yeniden açmayı deneyin." }, { "id": "plugin.api.update_user_status.bad_status", @@ -7635,10 +7635,6 @@ "id": "app.user.get_threads_for_user.app_error", "translation": "Kullanıcı konuları alınamadı" }, - { - "id": "api.templates.over_limit_fix_now", - "translation": "Şimdi düzelt" - }, { "id": "api.roles.patch_roles.not_allowed_permission.error", "translation": "Eklemek ya da silmek istediğiniz bir ya da bir kaç yetkiye izin verilmiyor" @@ -7701,11 +7697,11 @@ }, { "id": "api.templates.payment_failed.subject", - "translation": "İşlem gerekli: Mattermost Cloud ödemesi alınamadı" + "translation": "İşlem gerekli: Mattermost {{.Plan}} ödemesi alınamadı" }, { "id": "api.templates.payment_failed.info3", - "translation": "Mattermost Cloud aboneliğinizin kesintiye uğramaması için sorunu çözmesi için bankanızla görüşün ya da ödeme bilgilerinizi güncelleyin. Ödeme bilgileri güncellendikten sonra Mattermost kalan ödemeyi almayı deneyecek." + "translation": "Mattermost {{.Plan}} erişiminizin kesintiye uğramaması için sorunu çözmek amacıyla bankanızla görüşün ya da ödeme bilgilerinizi güncelleyin. Ödeme bilgileri güncellendikten sonra Mattermost kalan ödemeyi almayı deneyecek." }, { "id": "api.templates.payment_failed.info2", @@ -9529,5 +9525,9 @@ { "id": "app.cloud.trial_plan_bot_message", "translation": "{{.WorkspaceName}} çalışma alanının {{.UsersNum}} üyesi şuraya erişmek için Enterprise sürümü deneme süresinin başlatılmasını istedi: " + }, + { + "id": "app.cloud.get_current_plan_name.app_error", + "translation": "Geçerli tarifenin adı alınamadı" } ] diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json index e72cc7d333..4dba1375ff 100644 --- a/i18n/zh-CN.json +++ b/i18n/zh-CN.json @@ -7459,10 +7459,6 @@ "id": "ent.message_export.global_relay_export.get_attachment_error", "translation": "无法获取帖子的文件信息。" }, - { - "id": "api.templates.over_limit_fix_now", - "translation": "立刻修复" - }, { "id": "api.templates.email_us_anytime_at", "translation": "随时通过电子邮件发送给我们 " diff --git a/jobs/import_process/worker.go b/jobs/import_process/worker.go index be09177006..5747d04b93 100644 --- a/jobs/import_process/worker.go +++ b/jobs/import_process/worker.go @@ -17,6 +17,7 @@ import ( "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/services/configservice" "github.com/mattermost/mattermost-server/v6/shared/filestore" + "github.com/mattermost/mattermost-server/v6/shared/mlog" ) const jobName = "ImportProcess" @@ -28,10 +29,11 @@ type AppIface interface { FileSize(path string) (int64, *model.AppError) FileReader(path string) (filestore.ReadCloseSeeker, *model.AppError) BulkImportWithPath(c *request.Context, jsonlReader io.Reader, attachmentsReader *zip.Reader, dryRun bool, workers int, importPath string) (*model.AppError, int) + Log() *mlog.Logger } func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker { - appContext := request.EmptyContext(nil) + appContext := request.EmptyContext(app.Log()) isEnabled := func(cfg *model.Config) bool { return true } diff --git a/model/channel.go b/model/channel.go index e66c30faef..e900011cc9 100644 --- a/model/channel.go +++ b/model/channel.go @@ -296,8 +296,9 @@ func (o *Channel) PreSave() { o.Name = SanitizeUnicode(o.Name) o.DisplayName = SanitizeUnicode(o.DisplayName) - - o.CreateAt = GetMillis() + if o.CreateAt == 0 { + o.CreateAt = GetMillis() + } o.UpdateAt = o.CreateAt o.ExtraUpdateAt = 0 } diff --git a/model/config.go b/model/config.go index b117548a67..967cff1dc8 100644 --- a/model/config.go +++ b/model/config.go @@ -371,6 +371,7 @@ type ServiceSettings struct { EnableSVGs *bool `access:"site_posts"` EnableLatex *bool `access:"site_posts"` EnableInlineLatex *bool `access:"site_posts"` + PostPriority *bool `access:"site_posts"` EnableAPIChannelDeletion *bool EnableLocalMode *bool LocalModeSocketLocation *string // telemetry: none @@ -842,6 +843,10 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) { if s.EnableCustomGroups == nil { s.EnableCustomGroups = NewBool(true) } + + if s.PostPriority == nil { + s.PostPriority = NewBool(false) + } } type ClusterSettings struct { diff --git a/model/feature_flags.go b/model/feature_flags.go index 257b6e1a6d..209e7a9f99 100644 --- a/model/feature_flags.go +++ b/model/feature_flags.go @@ -73,6 +73,8 @@ type FeatureFlags struct { BoardsProduct bool PlanUpgradeButtonText string + + PostPriority bool } func (f *FeatureFlags) SetDefaults() { @@ -100,6 +102,7 @@ func (f *FeatureFlags) SetDefaults() { f.CallsEnabled = true f.BoardsProduct = false f.PlanUpgradeButtonText = "upgrade" + f.PostPriority = false } func (f *FeatureFlags) Plugins() map[string]string { diff --git a/model/manifest.go b/model/manifest.go index 9b1551e921..9c5047a887 100644 --- a/model/manifest.go +++ b/model/manifest.go @@ -364,8 +364,9 @@ func (s *PluginSetting) isValid() error { pluginSettingType == Text || pluginSettingType == LongText || pluginSettingType == Number || - pluginSettingType == Username) { - return errors.New("should not set Placeholder for setting type not in text, generated or username") + pluginSettingType == Username || + pluginSettingType == Custom) { + return errors.New("should not set Placeholder for setting type not in text, generated, number, username, or custom") } if s.Options != nil { diff --git a/model/manifest_test.go b/model/manifest_test.go index de93569b53..8f42ab5f4b 100644 --- a/model/manifest_test.go +++ b/model/manifest_test.go @@ -184,6 +184,13 @@ func TestSettingIsValid(t *testing.T) { }, false, }, + "Placeholder is allowed for custom settings": { + PluginSetting{ + Type: "custom", + Placeholder: "some Text", + }, + false, + }, } { t.Run(name, func(t *testing.T) { err := test.Setting.isValid() diff --git a/services/telemetry/telemetry.go b/services/telemetry/telemetry.go index fc58db9005..3168f61e35 100644 --- a/services/telemetry/telemetry.go +++ b/services/telemetry/telemetry.go @@ -448,6 +448,7 @@ func (ts *TelemetryService) trackConfig() { "enable_file_search": *cfg.ServiceSettings.EnableFileSearch, "restrict_link_previews": isDefault(*cfg.ServiceSettings.RestrictLinkPreviews, ""), "enable_custom_groups": *cfg.ServiceSettings.EnableCustomGroups, + "post_priority": *cfg.ServiceSettings.PostPriority, }) ts.SendTelemetry(TrackConfigTeam, map[string]any{ diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index 51a2dd1055..45b6277728 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -11089,7 +11089,7 @@ func (s *OpenTracingLayerUserStore) GetTeamGroupUsers(teamID string) ([]*model.U return result, err } -func (s *OpenTracingLayerUserStore) GetUnreadCount(userID string) (int64, error) { +func (s *OpenTracingLayerUserStore) GetUnreadCount(userID string, isCRTEnabled bool) (int64, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetUnreadCount") s.Root.Store.SetContext(newCtx) @@ -11098,7 +11098,7 @@ func (s *OpenTracingLayerUserStore) GetUnreadCount(userID string) (int64, error) }() defer span.Finish() - result, err := s.UserStore.GetUnreadCount(userID) + result, err := s.UserStore.GetUnreadCount(userID, isCRTEnabled) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index 2506d610f9..6e544138a2 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -12660,11 +12660,11 @@ func (s *RetryLayerUserStore) GetTeamGroupUsers(teamID string) ([]*model.User, e } -func (s *RetryLayerUserStore) GetUnreadCount(userID string) (int64, error) { +func (s *RetryLayerUserStore) GetUnreadCount(userID string, isCRTEnabled bool) (int64, error) { tries := 0 for { - result, err := s.UserStore.GetUnreadCount(userID) + result, err := s.UserStore.GetUnreadCount(userID, isCRTEnabled) if err == nil { return result, nil } diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index e772f2706d..19686d967d 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -4337,48 +4337,45 @@ func (s SqlChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userI LastActivityAt FROM ((SELECT - Posts.ChannelId AS ID, + PublicChannels.Id AS ID, 'O' AS Type, PublicChannels.DisplayName AS DisplayName, PublicChannels.Name AS Name, - count(Posts.Id) AS MessageCount, - max(Posts.CreateAt) AS LastActivityAt + COALESCE(count(Posts.Id), 0) AS MessageCount, + COALESCE(max(Posts.CreateAt), 0) AS LastActivityAt FROM - Posts - LEFT JOIN PublicChannels on Posts.ChannelId = PublicChannels.Id + PublicChannels + LEFT JOIN Posts on Posts.ChannelId = PublicChannels.Id AND Posts.Type = '' AND Posts.CreateAt > ? AND Posts.DeleteAt = 0 + LEFT JOIN Channels on Channels.Id = PublicChannels.Id WHERE - Posts.DeleteAt = 0 - AND Posts.CreateAt > ? - AND (Posts.Type = '' OR Posts.Type = 'system_join_channel') - AND PublicChannels.TeamId = ? + PublicChannels.TeamId = ? AND PublicChannels.DeleteAt = 0 + AND Channels.CreateAt < ? GROUP BY - Posts.ChannelId, + PublicChannels.Id, PublicChannels.DisplayName, PublicChannels.Name, PublicChannels.TeamId) UNION ALL (SELECT - Posts.ChannelId AS ID, + Channels.Id AS ID, Channels.Type AS Type, Channels.DisplayName AS DisplayName, Channels.Name AS Name, - count(Posts.Id) AS MessageCount, - max(Posts.CreateAt) AS LastActivityAt + COALESCE(count(Posts.Id), 0) AS MessageCount, + COALESCE(max(Posts.CreateAt), 0) AS LastActivityAt FROM - Posts - LEFT JOIN Channels on Posts.ChannelId = Channels.Id + Channels + LEFT JOIN Posts on Posts.ChannelId = Channels.Id AND Posts.Type = '' AND Posts.CreateAt > ? AND Posts.DeleteAt = 0 LEFT JOIN ChannelMembers on Posts.ChannelId = ChannelMembers.ChannelId WHERE - Posts.DeleteAt = 0 - AND Posts.CreateAt > ? - AND (Posts.Type = '' OR Posts.Type = 'system_join_channel') - AND Channels.TeamId = ? + Channels.TeamId = ? + AND Channels.CreateAt < ? AND Channels.Type = 'P' AND Channels.DeleteAt = 0 AND ChannelMembers.UserId = ? GROUP BY - Posts.ChannelId, + Channels.Id, Channels.Type, Channels.DisplayName, Channels.Name)) AS A @@ -4387,8 +4384,7 @@ func (s SqlChannelStore) GetTopInactiveChannelsForTeamSince(teamID string, userI Name ASC LIMIT ? OFFSET ?` - args = append(args, since, teamID, since, teamID, userID, limit+1, offset) - + args = append(args, since, teamID, since, since, teamID, since, userID, limit+1, offset) if err := s.GetReplicaX().Select(&channels, query, args...); err != nil { return nil, errors.Wrap(err, "failed to get top Channels") } @@ -4411,25 +4407,23 @@ func (s SqlChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userI query = ` SELECT - Posts.ChannelId AS ID, + Channels.Id AS ID, Channels.Type AS Type, Channels.DisplayName AS DisplayName, Channels.Name AS Name, - count(Posts.Id) AS MessageCount, - max(Posts.CreateAt) AS LastActivityAt + COALESCE(count(Posts.Id), 0) AS MessageCount, + COALESCE(max(Posts.CreateAt), 0) AS LastActivityAt FROM - Posts - LEFT JOIN Channels on Posts.ChannelId = Channels.Id + Channels + LEFT JOIN Posts on Posts.ChannelId = Channels.Id AND Posts.Type = '' AND Posts.CreateAt > ? AND Posts.DeleteAt = 0 LEFT JOIN ChannelMembers on Posts.ChannelId = ChannelMembers.ChannelId WHERE - Posts.DeleteAt = 0 - AND Posts.CreateAt > ? - AND (Posts.Type = '' OR Posts.Type = 'system_join_channel') - AND Channels.DeleteAt = 0 + Channels.DeleteAt = 0 + AND Channels.CreateAt < ? AND (Channels.Type = 'O' OR Channels.Type = 'P') AND ChannelMembers.UserId = ? ` - args = []any{since, userID} + args = []any{since, since, userID} if teamID != "" { query += ` @@ -4439,7 +4433,7 @@ func (s SqlChannelStore) GetTopInactiveChannelsForUserSince(teamID string, userI query += ` Group By - Posts.ChannelId, + Channels.Id, Channels.Type, Channels.DisplayName, Channels.Name diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index a8c8faf04b..2f3fd2b6b9 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -2999,21 +2999,21 @@ func (s *SqlPostStore) updateThreadsFromPosts(transaction *sqlxTxWrapper, posts } func (s *SqlPostStore) GetTopDMsForUserSince(userID string, since int64, offset int, limit int) (*model.TopDMList, error) { - var botsFilterExpr, stringSplitKeyword string - if s.DriverName() == model.DatabaseDriverPostgres { - stringSplitKeyword = "split_part" - } else if s.DriverName() == model.DatabaseDriverMysql { - stringSplitKeyword = "SUBSTRING_INDEX" - } - + var botsFilterExpr string /* Channel.Name is of the format userId1__userId2. Using this, self dms, and bot dms can be filtered. */ - botsFilterExpr = fmt.Sprintf(` - %s(Channels.Name, '__', 1) NOT IN (SELECT UserId FROM Bots) - AND %s(Channels.Name, '__', 2) NOT IN (SELECT UserId FROM Bots) - `, stringSplitKeyword, stringSplitKeyword) + if s.DriverName() == model.DatabaseDriverPostgres { + botsFilterExpr = `SPLIT_PART(Channels.Name, '__', 1) NOT IN (SELECT UserId FROM Bots) + AND SPLIT_PART(Channels.Name, '__', 2) NOT IN (SELECT UserId FROM Bots) + ` + } else if s.DriverName() == model.DatabaseDriverMysql { + botsFilterExpr = `SUBSTRING_INDEX(Channels.Name, '__', 1) NOT IN (SELECT UserId FROM Bots) + AND SUBSTRING_INDEX(Channels.Name, '__', -1) NOT IN (SELECT UserId FROM Bots) + ` + } + channelSelector := s.getQueryBuilder().Select("Id", "TotalMsgCount").From("Channels").Join("ChannelMembers as cm on cm.ChannelId = Channels.Id"). Where(sq.And{ sq.Expr("Channels.Type = 'D'"), diff --git a/store/sqlstore/user_store.go b/store/sqlstore/user_store.go index b5b60c3d57..201aac0ca3 100644 --- a/store/sqlstore/user_store.go +++ b/store/sqlstore/user_store.go @@ -1371,9 +1371,18 @@ func (us SqlUserStore) AnalyticsActiveCountForPeriod(startTime int64, endTime in return v, nil } -func (us SqlUserStore) GetUnreadCount(userId string) (int64, error) { +func (us SqlUserStore) GetUnreadCount(userId string, isCRTEnabled bool) (int64, error) { + var totalMsgCountColumn = "c.TotalMsgCount" + var msgCountColumn = "cm.MsgCount" + var mentionCountColumn = "cm.MentionCount" + if isCRTEnabled { + totalMsgCountColumn = "c.TotalMsgCountRoot" + msgCountColumn = "cm.MsgCountRoot" + mentionCountColumn = "cm.MentionCountRoot" + } + query := ` - SELECT SUM(CASE WHEN c.Type = ? THEN (c.TotalMsgCount - cm.MsgCount) ELSE cm.MentionCount END) + SELECT SUM(CASE WHEN c.Type = ? THEN (` + totalMsgCountColumn + ` - ` + msgCountColumn + `) ELSE ` + mentionCountColumn + ` END) FROM Channels c INNER JOIN ChannelMembers cm ON cm.ChannelId = c.Id @@ -1503,8 +1512,11 @@ func generateSearchQuery(query sq.SelectBuilder, terms []string, fields []string var dbSpecificTerm string if isPostgreSQL { - // Escaping the : in case of a Postgres search. - term = strings.ReplaceAll(term, ":", "\\:") + // Refer to https://www.postgresql.org/docs/current/functions-textsearch.html for the list of operators. + for _, c := range []string{":", "(", ")", "<", "!", "|"} { + // Escaping the special chars in case of a Postgres search. + term = strings.ReplaceAll(term, c, "\\"+c) + } } for _, field := range fields { diff --git a/store/store.go b/store/store.go index 0e392bf861..df2473b701 100644 --- a/store/store.go +++ b/store/store.go @@ -447,7 +447,7 @@ type UserStore interface { PermanentDelete(userID string) error AnalyticsActiveCount(timestamp int64, options model.UserCountOptions) (int64, error) AnalyticsActiveCountForPeriod(startTime int64, endTime int64, options model.UserCountOptions) (int64, error) - GetUnreadCount(userID string) (int64, error) + GetUnreadCount(userID string, isCRTEnabled bool) (int64, error) GetUnreadCountForChannel(userID string, channelID string) (int64, error) GetAnyUnreadPostCountForChannel(userID string, channelID string) (int64, error) GetRecentlyActiveUsersForTeam(teamID string, offset, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 913ac7e08a..7b2757ad14 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -7978,6 +7978,7 @@ func testGetTopInactiveChannels(t *testing.T, ss store.Store) { DisplayName: "test_share_flag asdf", Name: "test_share_flag_public0", Type: model.ChannelTypeOpen, + CreateAt: 1, } channelSaved0, err := ss.Channel().Save(channelPublic0, 999) @@ -7989,6 +7990,7 @@ func testGetTopInactiveChannels(t *testing.T, ss store.Store) { DisplayName: "test_share_flag", Name: "test_share_flag", Type: model.ChannelTypeOpen, + CreateAt: 1, } channelSaved1, err := ss.Channel().Save(channelPublic1, 999) @@ -8001,6 +8003,7 @@ func testGetTopInactiveChannels(t *testing.T, ss store.Store) { c3.DisplayName = "Channel3" + model.NewId() c3.Name = NewTestId() c3.Type = model.ChannelTypePrivate + c3.CreateAt = 1 channelPrivate, nErr := ss.Channel().Save(&c3, -1) require.NoError(t, nErr) @@ -8080,7 +8083,7 @@ func testGetTopInactiveChannels(t *testing.T, ss store.Store) { // for u1 t.Run("top inactive channels for team - u1 ", func(t *testing.T) { - topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForTeamSince(team.Id, u1.Id, 0, 0, 10) + topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForTeamSince(team.Id, u1.Id, 2, 0, 10) require.NoError(t, err) require.Len(t, topInactiveChannels.Items, 3) require.Equal(t, topInactiveChannels.Items[0].ID, channelSaved0.Id) @@ -8096,7 +8099,7 @@ func testGetTopInactiveChannels(t *testing.T, ss store.Store) { }) t.Run("top inactive channels for user - u1 ", func(t *testing.T) { - topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForUserSince(team.Id, u1.Id, 0, 0, 10) + topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForUserSince(team.Id, u1.Id, 2, 0, 10) require.NoError(t, err) require.Len(t, topInactiveChannels.Items, 2) require.Equal(t, topInactiveChannels.Items[0].ID, channelPrivate.Id) @@ -8105,7 +8108,7 @@ func testGetTopInactiveChannels(t *testing.T, ss store.Store) { // for u2 t.Run("top inactive channels for team - u2 ", func(t *testing.T) { - topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForTeamSince(team.Id, u2.Id, 0, 0, 10) + topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForTeamSince(team.Id, u2.Id, 2, 0, 10) require.NoError(t, err) require.Len(t, topInactiveChannels.Items, 2) require.Equal(t, topInactiveChannels.Items[0].ID, channelSaved0.Id) @@ -8114,7 +8117,7 @@ func testGetTopInactiveChannels(t *testing.T, ss store.Store) { }) t.Run("top inactive channels for user - u2 ", func(t *testing.T) { - topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForUserSince(team.Id, u2.Id, 0, 0, 10) + topInactiveChannels, err := ss.Channel().GetTopInactiveChannelsForUserSince(team.Id, u2.Id, 2, 0, 10) require.NoError(t, err) require.Len(t, topInactiveChannels.Items, 1) require.Equal(t, topInactiveChannels.Items[0].ID, channelPublic0.Id) diff --git a/store/storetest/mocks/UserStore.go b/store/storetest/mocks/UserStore.go index afb1ae0c9a..ea0fdcfcc0 100644 --- a/store/storetest/mocks/UserStore.go +++ b/store/storetest/mocks/UserStore.go @@ -960,20 +960,20 @@ func (_m *UserStore) GetTeamGroupUsers(teamID string) ([]*model.User, error) { return r0, r1 } -// GetUnreadCount provides a mock function with given fields: userID -func (_m *UserStore) GetUnreadCount(userID string) (int64, error) { - ret := _m.Called(userID) +// GetUnreadCount provides a mock function with given fields: userID, isCRTEnabled +func (_m *UserStore) GetUnreadCount(userID string, isCRTEnabled bool) (int64, error) { + ret := _m.Called(userID, isCRTEnabled) var r0 int64 - if rf, ok := ret.Get(0).(func(string) int64); ok { - r0 = rf(userID) + if rf, ok := ret.Get(0).(func(string, bool) int64); ok { + r0 = rf(userID, isCRTEnabled) } else { r0 = ret.Get(0).(int64) } var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(userID) + if rf, ok := ret.Get(1).(func(string, bool) error); ok { + r1 = rf(userID, isCRTEnabled) } else { r1 = ret.Error(1) } diff --git a/store/storetest/post_store.go b/store/storetest/post_store.go index b2214d83c8..41b8c138d3 100644 --- a/store/storetest/post_store.go +++ b/store/storetest/post_store.go @@ -4067,6 +4067,8 @@ func testGetTopDMsForUserSince(t *testing.T, ss store.Store, s SqlStore) { u2 := model.User{Email: MakeEmail(), Username: model.NewId()} u3 := model.User{Email: MakeEmail(), Username: model.NewId()} u4 := model.User{Email: MakeEmail(), Username: model.NewId()} + u5 := model.User{Email: MakeEmail(), Username: model.NewId()} + _, err := ss.User().Save(&user) require.NoError(t, err) _, err = ss.User().Save(&u1) @@ -4077,6 +4079,17 @@ func testGetTopDMsForUserSince(t *testing.T, ss store.Store, s SqlStore) { require.NoError(t, err) _, err = ss.User().Save(&u4) require.NoError(t, err) + _, err = ss.User().Save(&u5) + require.NoError(t, err) + bot := &model.Bot{ + Username: "bot_user", + Description: "bot", + OwnerId: model.NewId(), + UserId: u5.Id, + } + + savedBot, nErr := ss.Bot().Save(bot) + require.NoError(t, nErr) // user direct messages chUser1, nErr := ss.Channel().CreateDirectChannel(&u1, &user) require.NoError(t, nErr) @@ -4088,6 +4101,17 @@ func testGetTopDMsForUserSince(t *testing.T, ss store.Store, s SqlStore) { chUser3User4, nErr := ss.Channel().CreateDirectChannel(&u3, &u4) require.NoError(t, nErr) + // bot direct message - should be ignored by top DMs + botUser, err := ss.User().Get(context.Background(), savedBot.UserId) + require.NoError(t, err) + chBot, nErr := ss.Channel().CreateDirectChannel(&user, botUser) + require.NoError(t, nErr) + _, err = ss.Post().Save(&model.Post{ + ChannelId: chBot.Id, + UserId: botUser.Id, + }) + require.NoError(t, err) + // sample post data // for u1 _, err = ss.Post().Save(&model.Post{ diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index 14cde98222..fd2d8746d9 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -2440,14 +2440,23 @@ func testUserUnreadCount(t *testing.T, ss store.Store) { nErr = ss.Channel().IncrementMentionCount(c2.Id, []string{u2.Id}, false) require.NoError(t, nErr) - badge, unreadCountErr := ss.User().GetUnreadCount(u2.Id) + badge, unreadCountErr := ss.User().GetUnreadCount(u2.Id, false) require.NoError(t, unreadCountErr) require.Equal(t, int64(3), badge, "should have 3 unread messages") - badge, unreadCountErr = ss.User().GetUnreadCount(u3.Id) + badge, unreadCountErr = ss.User().GetUnreadCount(u3.Id, false) require.NoError(t, unreadCountErr) require.Equal(t, int64(1), badge, "should have 1 unread message") + // Increment root mentions by 1 + nErr = ss.Channel().IncrementMentionCount(c1.Id, []string{u3.Id}, true) + require.NoError(t, nErr) + + // CRT is enabled, only root mentions are counted + badge, unreadCountErr = ss.User().GetUnreadCount(u3.Id, true) + require.NoError(t, unreadCountErr) + require.Equal(t, int64(1), badge, "should have 1 unread message with CRT") + badge, unreadCountErr = ss.User().GetUnreadCountForChannel(u2.Id, c1.Id) require.NoError(t, unreadCountErr) require.Equal(t, int64(1), badge, "should have 1 unread messages for that channel") @@ -2807,6 +2816,20 @@ func testUserStoreSearch(t *testing.T, ss store.Store) { &model.UserSearchOptions{}, []*model.User{}, }, + { + "escape ( and )", + t1id, + "ji(bah)", + &model.UserSearchOptions{}, + []*model.User{}, + }, + { + "escape <", + t1id, + "ji(bah<", + &model.UserSearchOptions{}, + []*model.User{}, + }, { "wildcard search", t1id, diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index 6835edbb68..3a4e40ed39 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -9983,10 +9983,10 @@ func (s *TimerLayerUserStore) GetTeamGroupUsers(teamID string) ([]*model.User, e return result, err } -func (s *TimerLayerUserStore) GetUnreadCount(userID string) (int64, error) { +func (s *TimerLayerUserStore) GetUnreadCount(userID string, isCRTEnabled bool) (int64, error) { start := time.Now() - result, err := s.UserStore.GetUnreadCount(userID) + result, err := s.UserStore.GetUnreadCount(userID, isCRTEnabled) elapsed := float64(time.Since(start)) / float64(time.Second) if s.Root.Metrics != nil { diff --git a/web/handlers.go b/web/handlers.go index 777a8ebcad..f0faa7abe3 100644 --- a/web/handlers.go +++ b/web/handlers.go @@ -87,16 +87,17 @@ type Handler struct { } func generateDevCSP(c Context) string { + var devCSP []string + // Add unsafe-eval to the content security policy for faster source maps in development mode - devCSPMap := make(map[string]bool) if model.BuildNumber == "dev" { - devCSPMap["unsafe-eval"] = true + devCSP = append(devCSP, "'unsafe-eval'") } // Add unsafe-inline to unlock extensions like React & Redux DevTools in Firefox // see https://github.com/reduxjs/redux-devtools/issues/380 if model.BuildNumber == "dev" { - devCSPMap["unsafe-inline"] = true + devCSP = append(devCSP, "'unsafe-inline'") } // Add supported flags for debugging during development, even if not on a dev build. @@ -118,21 +119,29 @@ func generateDevCSP(c Context) string { // Honour only supported keys switch devFlagKey { case "unsafe-eval", "unsafe-inline": - devCSPMap[devFlagKey] = true + if model.BuildNumber == "dev" { + // These flags are added automatically for dev builds + continue + } + + devCSP = append(devCSP, "'"+devFlagKey+"'") default: c.Logger.Warn("Unrecognized developer flag", mlog.String("developer_flag", devFlagKVStr)) } } } - var devCSP string - supportedCSPFlags := []string{"unsafe-eval", "unsafe-inline"} - for _, devCSPFlag := range supportedCSPFlags { - if devCSPMap[devCSPFlag] { - devCSP += fmt.Sprintf(" '%s'", devCSPFlag) - } + + // Add flags for Webpack dev servers used by other products during development + if model.BuildNumber == "dev" { + // Focalboard runs on http://localhost:9006 + devCSP = append(devCSP, "http://localhost:9006") } - return devCSP + if len(devCSP) == 0 { + return "" + } + + return " " + strings.Join(devCSP, " ") } func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { diff --git a/web/handlers_test.go b/web/handlers_test.go index 4651bb99e6..906a464b39 100644 --- a/web/handlers_test.go +++ b/web/handlers_test.go @@ -388,7 +388,7 @@ func TestHandlerServeCSPHeader(t *testing.T) { response := httptest.NewRecorder() handler.ServeHTTP(response, request) assert.Equal(t, 200, response.Code) - assert.Equal(t, []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com 'unsafe-eval' 'unsafe-inline'"}, response.Header()["Content-Security-Policy"]) + assert.Equal(t, []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com 'unsafe-eval' 'unsafe-inline' http://localhost:9006"}, response.Header()["Content-Security-Policy"]) }) } @@ -411,9 +411,9 @@ func TestGenerateDevCSP(t *testing.T) { devCSP := generateDevCSP(*c) - assert.Equal(t, " 'unsafe-eval' 'unsafe-inline'", devCSP) - + assert.Equal(t, " 'unsafe-eval' 'unsafe-inline' http://localhost:9006", devCSP) }) + t.Run("allowed dev flags", func(t *testing.T) { th := Setup(t) defer th.TearDown() @@ -436,7 +436,7 @@ func TestGenerateDevCSP(t *testing.T) { devCSP := generateDevCSP(*c) - assert.Equal(t, " 'unsafe-eval' 'unsafe-inline'", devCSP) + assert.Equal(t, " 'unsafe-inline' 'unsafe-eval'", devCSP) }) t.Run("partial dev flags", func(t *testing.T) {