diff --git a/models/activities/notification_list.go b/models/activities/notification_list.go index 0cbb91df3c..ee2ced93ff 100644 --- a/models/activities/notification_list.go +++ b/models/activities/notification_list.go @@ -156,6 +156,14 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n continue } + // Filter users by their notification preference. + // At this point we exclude: + // user that don't have all notifications enabled or users only get notification on mention and this is one ... + if !(user.UINotificationsPreference == user_model.NotificationsEnabled || + user.UINotificationsPreference == user_model.NotificationsOnMention) { + continue + } + if notificationExists(notifications, issue.ID, userID) { if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil { return err diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 8504d88ce5..ef0e88247b 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -8,6 +8,7 @@ email: user1@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -45,6 +46,7 @@ email: user2@example.com keep_email_private: true email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -82,6 +84,7 @@ email: org3@example.com keep_email_private: false email_notifications_preference: onmention + ui_notifications_preference: onmention passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -119,6 +122,7 @@ email: user4@example.com keep_email_private: false email_notifications_preference: onmention + ui_notifications_preference: onmention passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -156,6 +160,7 @@ email: user5@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -193,6 +198,7 @@ email: org6@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -230,6 +236,7 @@ email: org7@example.com keep_email_private: false email_notifications_preference: disabled + ui_notifications_preference: disabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -267,6 +274,7 @@ email: user8@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -304,6 +312,7 @@ email: user9@example.com keep_email_private: false email_notifications_preference: onmention + ui_notifications_preference: onmention passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -341,6 +350,7 @@ email: user10@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -378,6 +388,7 @@ email: user11@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -415,6 +426,7 @@ email: user12@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -452,6 +464,7 @@ email: user13@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -489,6 +502,7 @@ email: user14@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -526,6 +540,7 @@ email: user15@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -563,6 +578,7 @@ email: user16@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -600,6 +616,7 @@ email: org17@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -637,6 +654,7 @@ email: user18@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -674,6 +692,7 @@ email: org19@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -711,6 +730,7 @@ email: user20@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -748,6 +768,7 @@ email: user21@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -785,6 +806,7 @@ email: limited_org@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -822,6 +844,7 @@ email: privated_org@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -859,6 +882,7 @@ email: user24@example.com keep_email_private: true email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -896,6 +920,7 @@ email: org25@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -933,6 +958,7 @@ email: org26@example.com keep_email_private: false email_notifications_preference: onmention + ui_notifications_preference: onmention passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -970,6 +996,7 @@ email: user27@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -1007,6 +1034,7 @@ email: user28@example.com keep_email_private: true email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -1044,6 +1072,7 @@ email: user29@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -1081,6 +1110,7 @@ email: user30@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -1118,6 +1148,7 @@ email: user31@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -1155,6 +1186,7 @@ email: user32@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:notpassword passwd_hash_algo: dummy must_change_password: false @@ -1192,6 +1224,7 @@ email: user33@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -1230,6 +1263,7 @@ email: user34@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -1267,6 +1301,7 @@ email: private_org35@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -1304,6 +1339,7 @@ email: abcde@gitea.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -1341,6 +1377,7 @@ email: user37@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -1378,6 +1415,7 @@ email: user38@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -1415,6 +1453,7 @@ email: user39@example.com keep_email_private: false email_notifications_preference: enabled + ui_notifications_preference: enabled passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -1452,6 +1491,7 @@ email: user40@example.com keep_email_private: false email_notifications_preference: onmention + ui_notifications_preference: onmention passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false @@ -1489,6 +1529,7 @@ email: org41@example.com keep_email_private: false email_notifications_preference: onmention + ui_notifications_preference: onmention passwd: ZogKvWdyEx:password passwd_hash_algo: dummy must_change_password: false diff --git a/models/user/user.go b/models/user/user.go index 23637f4616..24e6fce155 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -61,15 +61,16 @@ const ( UserTypeRemoteUser ) +// Constants used as user-setting for both Email and UI notifications. const ( - // EmailNotificationsEnabled indicates that the user would like to receive all email notifications except your own - EmailNotificationsEnabled = "enabled" - // EmailNotificationsOnMention indicates that the user would like to be notified via email when mentioned. - EmailNotificationsOnMention = "onmention" - // EmailNotificationsDisabled indicates that the user would not like to be notified via email. - EmailNotificationsDisabled = "disabled" - // EmailNotificationsAndYourOwn indicates that the user would like to receive all email notifications and your own - EmailNotificationsAndYourOwn = "andyourown" + // NotificationsEnabled indicates that the user would like to receive all notifications except your own + NotificationsEnabled = "enabled" + // NotificationsOnMention indicates that the user would like to be notified when mentioned. + NotificationsOnMention = "onmention" + // NotificationsDisabled indicates that the user would not like to be notified. + NotificationsDisabled = "disabled" + // NotificationsAndYourOwn indicates that the user would like to receive all notifications and their own + NotificationsAndYourOwn = "andyourown" ) // User represents the object of individual and member of organization. @@ -82,6 +83,7 @@ type User struct { Email string `xorm:"NOT NULL"` KeepEmailPrivate bool EmailNotificationsPreference string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'enabled'"` + UINotificationsPreference string `xorm:"VARCHAR(20) NOT NULL DEFAULT 'enabled'"` Passwd string `xorm:"NOT NULL"` PasswdHashAlgo string `xorm:"NOT NULL DEFAULT 'argon2'"` @@ -578,6 +580,7 @@ type CreateUserOverwriteOptions struct { Visibility *structs.VisibleType AllowCreateOrganization optional.Option[bool] EmailNotificationsPreference *string + UINotificationsPreference *string MaxRepoCreation *int Theme *string IsRestricted optional.Option[bool] @@ -605,6 +608,8 @@ func createUser(ctx context.Context, u *User, createdByAdmin bool, overwriteDefa u.Visibility = setting.Service.DefaultUserVisibilityMode u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification + u.UINotificationsPreference = setting.Admin.DefaultUINotification + u.MaxRepoCreation = -1 u.Theme = setting.UI.DefaultTheme u.IsRestricted = setting.Service.DefaultUserIsRestricted @@ -630,6 +635,9 @@ func createUser(ctx context.Context, u *User, createdByAdmin bool, overwriteDefa if overwrite.EmailNotificationsPreference != nil { u.EmailNotificationsPreference = *overwrite.EmailNotificationsPreference } + if overwrite.UINotificationsPreference != nil { + u.UINotificationsPreference = *overwrite.UINotificationsPreference + } if overwrite.MaxRepoCreation != nil { u.MaxRepoCreation = *overwrite.MaxRepoCreation } @@ -924,7 +932,7 @@ func GetUserEmailsByNames(ctx context.Context, names []string) []string { if err != nil { continue } - if u.IsMailable() && u.EmailNotificationsPreference != EmailNotificationsDisabled { + if u.IsMailable() && u.EmailNotificationsPreference != NotificationsDisabled { mails = append(mails, u.Email) } } @@ -944,7 +952,7 @@ func GetMaileableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([ Where("`type` = ?", UserTypeIndividual). And("`prohibit_login` = ?", false). And("`is_active` = ?", true). - In("`email_notifications_preference`", EmailNotificationsEnabled, EmailNotificationsOnMention, EmailNotificationsAndYourOwn). + In("`email_notifications_preference`", NotificationsEnabled, NotificationsOnMention, NotificationsAndYourOwn). Find(&ous) } @@ -953,7 +961,7 @@ func GetMaileableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([ Where("`type` = ?", UserTypeIndividual). And("`prohibit_login` = ?", false). And("`is_active` = ?", true). - In("`email_notifications_preference`", EmailNotificationsEnabled, EmailNotificationsAndYourOwn). + In("`email_notifications_preference`", NotificationsEnabled, NotificationsAndYourOwn). Find(&ous) } diff --git a/models/user/user_test.go b/models/user/user_test.go index c4e278caab..2df82c1f95 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -137,21 +137,43 @@ func TestEmailNotificationPreferences(t *testing.T) { expected string userID int64 }{ - {user_model.EmailNotificationsEnabled, 1}, - {user_model.EmailNotificationsEnabled, 2}, - {user_model.EmailNotificationsOnMention, 3}, - {user_model.EmailNotificationsOnMention, 4}, - {user_model.EmailNotificationsEnabled, 5}, - {user_model.EmailNotificationsEnabled, 6}, - {user_model.EmailNotificationsDisabled, 7}, - {user_model.EmailNotificationsEnabled, 8}, - {user_model.EmailNotificationsOnMention, 9}, + {user_model.NotificationsEnabled, 1}, + {user_model.NotificationsEnabled, 2}, + {user_model.NotificationsOnMention, 3}, + {user_model.NotificationsOnMention, 4}, + {user_model.NotificationsEnabled, 5}, + {user_model.NotificationsEnabled, 6}, + {user_model.NotificationsDisabled, 7}, + {user_model.NotificationsEnabled, 8}, + {user_model.NotificationsOnMention, 9}, } { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.userID}) assert.Equal(t, test.expected, user.EmailNotificationsPreference) } } +func TestUINotificationPreferences(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + for _, test := range []struct { + expected string + userID int64 + }{ + {user_model.NotificationsEnabled, 1}, + {user_model.NotificationsEnabled, 2}, + {user_model.NotificationsOnMention, 3}, + {user_model.NotificationsOnMention, 4}, + {user_model.NotificationsEnabled, 5}, + {user_model.NotificationsEnabled, 6}, + {user_model.NotificationsDisabled, 7}, + {user_model.NotificationsEnabled, 8}, + {user_model.NotificationsOnMention, 9}, + } { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.userID}) + assert.Equal(t, test.expected, user.UINotificationsPreference) + } +} + func TestHashPasswordDeterministic(t *testing.T) { b := make([]byte, 16) u := &user_model.User{} diff --git a/modules/setting/admin.go b/modules/setting/admin.go index 8aebc76154..9a5ba86ae3 100644 --- a/modules/setting/admin.go +++ b/modules/setting/admin.go @@ -11,6 +11,7 @@ import ( var Admin struct { DisableRegularOrgCreation bool DefaultEmailNotification string + DefaultUINotification string UserDisabledFeatures container.Set[string] ExternalUserDisableFeatures container.Set[string] } @@ -19,6 +20,7 @@ func loadAdminFrom(rootCfg ConfigProvider) { sec := rootCfg.Section("admin") Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false) Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled") + Admin.DefaultUINotification = sec.Key("DEFAULT_UI_NOTIFICATIONS").MustString("enabled") Admin.UserDisabledFeatures = container.SetOf(sec.Key("USER_DISABLED_FEATURES").Strings(",")...) Admin.ExternalUserDisableFeatures = container.SetOf(sec.Key("EXTERNAL_USER_DISABLE_FEATURES").Strings(",")...) } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index fbada5472c..1283d41ce2 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -680,6 +680,7 @@ block.list.none = You have not blocked any users. profile = Profile account = Account appearance = Appearance +notifications = Notifications password = Password security = Security avatar = Avatar @@ -694,6 +695,7 @@ account_link = Linked Accounts organization = Organizations uid = UID webauthn = Two-Factor Authentication (Security Keys) +manage_notifications = Manage Notifications public_profile = Public Profile biography_placeholder = Tell us a little bit about yourself! (You can use Markdown) @@ -763,6 +765,8 @@ manage_emails = Manage Email Addresses manage_themes = Select default theme manage_openid = Manage OpenID Addresses email_desc = Your primary email address will be used for notifications, password recovery and, provided that it is not hidden, web-based Git operations. +email_notifications_desc = Choose what type of Email notifications you want to receive. +ui_notifications_desc = Choose what type of UI notifications you want to receive. theme_desc = This will be your default theme across the site. theme_colorblindness_help = Colorblindness Theme Support theme_colorblindness_prompt = Gitea just gets some themes with basic colorblindness support, which only have a few colors defined. The work is still in progress. More improvements could be done by defining more colors in the theme CSS files. @@ -789,6 +793,7 @@ add_openid = Add OpenID URI add_email_confirmation_sent = A confirmation email has been sent to "%s". Please check your inbox within the next %s to confirm your email address. add_email_success = The new email address has been added. email_preference_set_success = Email preference has been set successfully. +ui_preference_set_success = UI preference has been set successfully. add_openid_success = The new OpenID address has been added. keep_email_private = Hide Email Address keep_email_private_popup = This will hide your email address from your profile, as well as when you make a pull request or edit a file using the web interface. Pushed commits will not be modified. Use %s in commits to associate them with your account. @@ -976,11 +981,12 @@ confirm_delete_account = Confirm Deletion delete_account_title = Delete User Account delete_account_desc = Are you sure you want to permanently delete this user account? -email_notifications.enable = Enable Email Notifications -email_notifications.onmention = Only Email on Mention -email_notifications.disable = Disable Email Notifications -email_notifications.submit = Set Email Preference -email_notifications.andyourown = And Your Own Notifications +notifications.enable = Enable Notifications +notifications.onmention = Only Notify on Mention +notifications.andyourown = Receive All And Your Own +notifications.disable = Disable Notifications +notifications.submit_email = Set Email Preference +notifications.submit_ui = Set UI Preference visibility = User visibility visibility.public = Public diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go index 8ea7548e51..6b44625c27 100644 --- a/routers/web/user/setting/account.go +++ b/routers/web/user/setting/account.go @@ -154,10 +154,10 @@ func EmailPost(ctx *context.Context) { // Set Email Notification Preference if ctx.FormString("_method") == "NOTIFICATION" { preference := ctx.FormString("preference") - if !(preference == user_model.EmailNotificationsEnabled || - preference == user_model.EmailNotificationsOnMention || - preference == user_model.EmailNotificationsDisabled || - preference == user_model.EmailNotificationsAndYourOwn) { + if !(preference == user_model.NotificationsEnabled || + preference == user_model.NotificationsOnMention || + preference == user_model.NotificationsDisabled || + preference == user_model.NotificationsAndYourOwn) { log.Error("Email notifications preference change returned unrecognized option %s: %s", preference, ctx.Doer.Name) ctx.ServerError("SetEmailPreference", errors.New("option unrecognized")) return @@ -316,6 +316,8 @@ func loadAccountData(ctx *context.Context) { } ctx.Data["Emails"] = emails ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotificationsPreference + ctx.Data["UINotificationsPreference"] = ctx.Doer.UINotificationsPreference + ctx.Data["ActivationsPending"] = pendingActivation ctx.Data["CanAddEmails"] = !pendingActivation || !setting.Service.RegisterEmailConfirm ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer) diff --git a/routers/web/user/setting/notifications.go b/routers/web/user/setting/notifications.go new file mode 100644 index 0000000000..5227b3f406 --- /dev/null +++ b/routers/web/user/setting/notifications.go @@ -0,0 +1,116 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "errors" + "net/http" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/user" +) + +const ( + tplSettingsNotifications base.TplName = "user/settings/notifications" +) + +// Notifications render manage access token page +func Notifications(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings.notifications") + ctx.Data["PageIsSettingsNotifications"] = true + ctx.Data["Email"] = ctx.Doer.Email + ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail + + loadNotificationsData(ctx) + + ctx.HTML(http.StatusOK, tplSettingsNotifications) +} + +// NotificationPost response for change user's notification preferences +func NotificationPost(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsNotifications"] = true + + // Set Email Notification Preference + if ctx.FormString("_method") == "EMAIL" { + preference := ctx.FormString("preference") + if !(preference == user_model.NotificationsEnabled || + preference == user_model.NotificationsOnMention || + preference == user_model.NotificationsDisabled || + preference == user_model.NotificationsAndYourOwn) { + log.Error("Email notifications preference change returned unrecognized option %s: %s", preference, ctx.Doer.Name) + ctx.ServerError("SetEmailPreference", errors.New("option unrecognized")) + return + } + opts := &user.UpdateOptions{ + EmailNotificationsPreference: optional.Some(preference), + } + if err := user.UpdateUser(ctx, ctx.Doer, opts); err != nil { + log.Error("Set Email Notifications failed: %v", err) + ctx.ServerError("UpdateUser", err) + return + } + log.Trace("Email notifications preference made %s: %s", preference, ctx.Doer.Name) + ctx.Flash.Success(ctx.Tr("settings.email_preference_set_success")) + ctx.Redirect(setting.AppSubURL + "/user/settings/notifications") + return + // Set UI Notification Preference + } else if ctx.FormString("_method") == "UI" { + preference := ctx.FormString("preference") + if !(preference == user_model.NotificationsEnabled || + preference == user_model.NotificationsOnMention || + preference == user_model.NotificationsDisabled || + preference == user_model.NotificationsAndYourOwn) { + log.Error("UI notifications preference change returned unrecognized option %s: %s", preference, ctx.Doer.Name) + ctx.ServerError("SetUIPreference", errors.New("option unrecognized")) + return + } + opts := &user.UpdateOptions{ + UINotificationsPreference: optional.Some(preference), + } + if err := user.UpdateUser(ctx, ctx.Doer, opts); err != nil { + log.Error("Set UI Notifications failed: %v", err) + ctx.ServerError("UpdateUser", err) + return + } + log.Trace("UI notifications preference made %s: %s", preference, ctx.Doer.Name) + ctx.Flash.Success(ctx.Tr("settings.ui_preference_set_success")) + ctx.Redirect(setting.AppSubURL + "/user/settings/notifications") + return + } + + if ctx.HasError() { + loadAccountData(ctx) + ctx.HTML(http.StatusOK, tplSettingsAccount) + return + } +} + +func loadNotificationsData(ctx *context.Context) { + emlist, err := user_model.GetEmailAddresses(ctx, ctx.Doer.ID) + if err != nil { + ctx.ServerError("GetEmailAddresses", err) + return + } + type UserEmail struct { + user_model.EmailAddress + } + emails := make([]*UserEmail, len(emlist)) + for i, em := range emlist { + if !em.IsActivated { + continue + } + var email UserEmail + email.EmailAddress = *em + emails[i] = &email + } + ctx.Data["Emails"] = emails + ctx.Data["EmailNotificationsPreference"] = ctx.Doer.EmailNotificationsPreference + ctx.Data["UINotificationsPreference"] = ctx.Doer.UINotificationsPreference +} diff --git a/routers/web/web.go b/routers/web/web.go index 5fb1ce0e80..c160926c02 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -560,6 +560,11 @@ func registerRoutes(m *web.Route) { m.Get("/change_password", auth.MustChangePassword) m.Post("/change_password", web.Bind(forms.MustChangePasswordForm{}), auth.MustChangePasswordPost) m.Post("/avatar", web.Bind(forms.AvatarForm{}), user_setting.AvatarPost) + m.Group("/notifications", func() { + m.Get("", user_setting.Notifications) + m.Post("", user_setting.NotificationPost) + }) + m.Post("/avatar/delete", user_setting.DeleteAvatar) m.Group("/account", func() { m.Combo("").Get(user_setting.Account).Post(web.Bind(forms.ChangePasswordForm{}), user_setting.AccountPost) diff --git a/services/auth/sspi.go b/services/auth/sspi.go index 64a127e97a..5c3527ecd6 100644 --- a/services/auth/sspi.go +++ b/services/auth/sspi.go @@ -170,11 +170,12 @@ func (s *SSPI) newUser(ctx context.Context, username string, cfg *sspi.Source) ( Email: email, Language: cfg.DefaultLanguage, } - emailNotificationPreference := user_model.EmailNotificationsDisabled + notificationPreference := user_model.NotificationsDisabled overwriteDefault := &user_model.CreateUserOverwriteOptions{ IsActive: optional.Some(cfg.AutoActivateUsers), KeepEmailPrivate: optional.Some(true), - EmailNotificationsPreference: &emailNotificationPreference, + EmailNotificationsPreference: ¬ificationPreference, + UINotificationsPreference: ¬ificationPreference, } if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil { return nil, err diff --git a/services/mailer/mail_issue.go b/services/mailer/mail_issue.go index fab3315be2..16c5e7a20e 100644 --- a/services/mailer/mail_issue.go +++ b/services/mailer/mail_issue.go @@ -93,7 +93,7 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []*user_mo visited := make(container.Set[int64], len(unfiltered)+len(mentions)+1) // Avoid mailing the doer - if ctx.Doer.EmailNotificationsPreference != user_model.EmailNotificationsAndYourOwn && !ctx.ForceDoerNotification { + if ctx.Doer.EmailNotificationsPreference != user_model.NotificationsAndYourOwn && !ctx.ForceDoerNotification { visited.Add(ctx.Doer.ID) } @@ -134,9 +134,9 @@ func mailIssueCommentBatch(ctx *mailCommentContext, users []*user_model.User, vi } // At this point we exclude: // user that don't have all mails enabled or users only get mail on mention and this is one ... - if !(user.EmailNotificationsPreference == user_model.EmailNotificationsEnabled || - user.EmailNotificationsPreference == user_model.EmailNotificationsAndYourOwn || - fromMention && user.EmailNotificationsPreference == user_model.EmailNotificationsOnMention) { + if !(user.EmailNotificationsPreference == user_model.NotificationsEnabled || + user.EmailNotificationsPreference == user_model.NotificationsAndYourOwn || + fromMention && user.EmailNotificationsPreference == user_model.NotificationsOnMention) { continue } diff --git a/services/mailer/notify.go b/services/mailer/notify.go index e48b5d399d..5b7af50d8d 100644 --- a/services/mailer/notify.go +++ b/services/mailer/notify.go @@ -114,7 +114,7 @@ func (m *mailNotifier) PullRequestCodeComment(ctx context.Context, pr *issues_mo func (m *mailNotifier) IssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) { // mail only sent to added assignees and not self-assignee - if !removed && doer.ID != assignee.ID && assignee.EmailNotificationsPreference != user_model.EmailNotificationsDisabled { + if !removed && doer.ID != assignee.ID && assignee.EmailNotificationsPreference != user_model.NotificationsDisabled { ct := fmt.Sprintf("Assigned #%d.", issue.Index) if err := SendIssueAssignedMail(ctx, issue, doer, ct, comment, []*user_model.User{assignee}); err != nil { log.Error("Error in SendIssueAssignedMail for issue[%d] to assignee[%d]: %v", issue.ID, assignee.ID, err) @@ -123,7 +123,7 @@ func (m *mailNotifier) IssueChangeAssignee(ctx context.Context, doer *user_model } func (m *mailNotifier) PullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) { - if isRequest && doer.ID != reviewer.ID && reviewer.EmailNotificationsPreference != user_model.EmailNotificationsDisabled { + if isRequest && doer.ID != reviewer.ID && reviewer.EmailNotificationsPreference != user_model.NotificationsDisabled { ct := fmt.Sprintf("Requested to review %s.", issue.HTMLURL()) if err := SendIssueAssignedMail(ctx, issue, doer, ct, comment, []*user_model.User{reviewer}); err != nil { log.Error("Error in SendIssueAssignedMail for issue[%d] to reviewer[%d]: %v", issue.ID, reviewer.ID, err) diff --git a/services/uinotification/notify.go b/services/uinotification/notify.go index be5f7019a2..ff476d0fa3 100644 --- a/services/uinotification/notify.go +++ b/services/uinotification/notify.go @@ -75,6 +75,11 @@ func (ns *notificationService) CreateIssueComment(ctx context.Context, doer *use } _ = ns.issueQueue.Push(opts) for _, mention := range mentions { + // Avoid notifying users according to their setting. + if mention.UINotificationsPreference != user_model.NotificationsEnabled && + mention.UINotificationsPreference != user_model.NotificationsOnMention { + continue + } opts := issueNotificationOpts{ IssueID: issue.ID, NotificationAuthorID: doer.ID, @@ -92,7 +97,13 @@ func (ns *notificationService) NewIssue(ctx context.Context, issue *issues_model IssueID: issue.ID, NotificationAuthorID: issue.Poster.ID, }) + for _, mention := range mentions { + // Avoid notifying users according to their setting. + if mention.UINotificationsPreference != user_model.NotificationsEnabled && + mention.UINotificationsPreference != user_model.NotificationsOnMention { + continue + } _ = ns.issueQueue.Push(issueNotificationOpts{ IssueID: issue.ID, NotificationAuthorID: issue.Poster.ID, @@ -145,6 +156,15 @@ func (ns *notificationService) NewPullRequest(ctx context.Context, pr *issues_mo return } for _, id := range repoWatchers { + // Exclude users based on their notification prefs. + user, err := user_model.GetUserByID(ctx, id) + if err != nil { + log.Error("GetUserByID: %v", err) + return + } + if user.UINotificationsPreference != user_model.NotificationsEnabled { + continue + } toNotify.Add(id) } issueParticipants, err := issues_model.GetParticipantsIDsByIssueID(ctx, pr.IssueID) @@ -153,12 +173,30 @@ func (ns *notificationService) NewPullRequest(ctx context.Context, pr *issues_mo return } for _, id := range issueParticipants { + // Exclude users based on their notification prefs. + user, err := user_model.GetUserByID(ctx, id) + if err != nil { + log.Error("GetUserByID: %v", err) + return + } + if user.UINotificationsPreference != user_model.NotificationsEnabled { + continue + } toNotify.Add(id) } - delete(toNotify, pr.Issue.PosterID) + // Check if user should not be mentioned on their own actions. + if pr.Issue.Poster.UINotificationsPreference != user_model.NotificationsAndYourOwn { + delete(toNotify, pr.Issue.PosterID) + } for _, mention := range mentions { + // Exclude users based on their notification preferences. + if mention.UINotificationsPreference != user_model.NotificationsEnabled && + mention.UINotificationsPreference != user_model.NotificationsOnMention { + continue + } toNotify.Add(mention.ID) } + // Exclude users based on their notification preferences. for receiverID := range toNotify { _ = ns.issueQueue.Push(issueNotificationOpts{ IssueID: pr.Issue.ID, @@ -178,6 +216,11 @@ func (ns *notificationService) PullRequestReview(ctx context.Context, pr *issues } _ = ns.issueQueue.Push(opts) for _, mention := range mentions { + // Exclude users based on their notification preferences. + if mention.UINotificationsPreference != user_model.NotificationsEnabled && + mention.UINotificationsPreference != user_model.NotificationsOnMention { + continue + } opts := issueNotificationOpts{ IssueID: pr.Issue.ID, NotificationAuthorID: r.Reviewer.ID, @@ -192,6 +235,11 @@ func (ns *notificationService) PullRequestReview(ctx context.Context, pr *issues func (ns *notificationService) PullRequestCodeComment(ctx context.Context, pr *issues_model.PullRequest, c *issues_model.Comment, mentions []*user_model.User) { for _, mention := range mentions { + // Exclude users based on their notification preferences. + if mention.UINotificationsPreference != user_model.NotificationsEnabled && + mention.UINotificationsPreference != user_model.NotificationsOnMention { + continue + } _ = ns.issueQueue.Push(issueNotificationOpts{ IssueID: pr.Issue.ID, NotificationAuthorID: c.Poster.ID, diff --git a/services/user/update.go b/services/user/update.go index cbaf90053a..5290544b74 100644 --- a/services/user/update.go +++ b/services/user/update.go @@ -35,6 +35,7 @@ type UpdateOptions struct { IsActive optional.Option[bool] IsAdmin optional.Option[bool] EmailNotificationsPreference optional.Option[string] + UINotificationsPreference optional.Option[string] SetLastLogin bool RepoAdminChangeTeamAccess optional.Option[bool] } @@ -152,6 +153,12 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er cols = append(cols, "email_notifications_preference") } + if opts.UINotificationsPreference.Has() { + u.UINotificationsPreference = opts.UINotificationsPreference.Value() + + cols = append(cols, "ui_notifications_preference") + } + if opts.SetLastLogin { u.SetLastLogin() diff --git a/services/user/update_test.go b/services/user/update_test.go index fc24a6c212..b4f2aa2e7f 100644 --- a/services/user/update_test.go +++ b/services/user/update_test.go @@ -46,6 +46,7 @@ func TestUpdateUser(t *testing.T) { DiffViewStyle: optional.Some("split"), AllowCreateOrganization: optional.Some(false), EmailNotificationsPreference: optional.Some("disabled"), + UINotificationsPreference: optional.Some("disabled"), SetLastLogin: true, } assert.NoError(t, UpdateUser(db.DefaultContext, user, opts)) @@ -68,6 +69,7 @@ func TestUpdateUser(t *testing.T) { assert.Equal(t, opts.DiffViewStyle.Value(), user.DiffViewStyle) assert.Equal(t, opts.AllowCreateOrganization.Value(), user.AllowCreateOrganization) assert.Equal(t, opts.EmailNotificationsPreference.Value(), user.EmailNotificationsPreference) + assert.Equal(t, opts.UINotificationsPreference.Value(), user.UINotificationsPreference) user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28}) assert.Equal(t, opts.KeepEmailPrivate.Value(), user.KeepEmailPrivate) @@ -88,6 +90,7 @@ func TestUpdateUser(t *testing.T) { assert.Equal(t, opts.DiffViewStyle.Value(), user.DiffViewStyle) assert.Equal(t, opts.AllowCreateOrganization.Value(), user.AllowCreateOrganization) assert.Equal(t, opts.EmailNotificationsPreference.Value(), user.EmailNotificationsPreference) + assert.Equal(t, opts.UINotificationsPreference.Value(), user.UINotificationsPreference) } func TestUpdateAuth(t *testing.T) { diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl index 2aaf8535d1..e45f69c2e3 100644 --- a/templates/user/settings/account.tmpl +++ b/templates/user/settings/account.tmpl @@ -40,29 +40,6 @@
- {{if $.EnableNotifyMail}} -
-
{{ctx.Locale.Tr "settings.email_desc"}}
-
- {{$.CsrfTokenHtml}} - -
- - -
-
-
- {{end}} {{range .Emails}}
{{if not .IsPrimary}} diff --git a/templates/user/settings/navbar.tmpl b/templates/user/settings/navbar.tmpl index c360944814..66e7d45e11 100644 --- a/templates/user/settings/navbar.tmpl +++ b/templates/user/settings/navbar.tmpl @@ -10,6 +10,9 @@ {{ctx.Locale.Tr "settings.appearance"}} + + {{ctx.Locale.Tr "settings.notifications"}} + {{ctx.Locale.Tr "settings.security"}} diff --git a/templates/user/settings/notifications.tmpl b/templates/user/settings/notifications.tmpl new file mode 100644 index 0000000000..6f4aa48c69 --- /dev/null +++ b/templates/user/settings/notifications.tmpl @@ -0,0 +1,96 @@ +{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings notifications")}} +
+

+ {{ctx.Locale.Tr "settings.manage_notifications"}} +

+ +
+
+ {{if $.EnableNotifyMail}} +
{{ctx.Locale.Tr "settings.email_notifications_desc"}}
+
+ {{$.CsrfTokenHtml}} + + + +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ + +
+ {{end}} + +
+
+ +
+
+
{{ctx.Locale.Tr "settings.ui_notifications_desc"}}
+
+ {{$.CsrfTokenHtml}} + + + +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ + +
+ +
+
+{{template "user/settings/layout_footer" .}}