diff --git a/models/activities/notification.go b/models/activities/notification.go
index 211e84f418..8e2b6d6937 100644
--- a/models/activities/notification.go
+++ b/models/activities/notification.go
@@ -12,12 +12,8 @@ import (
 	"code.gitea.io/gitea/models/db"
 	issues_model "code.gitea.io/gitea/models/issues"
 	"code.gitea.io/gitea/models/organization"
-	access_model "code.gitea.io/gitea/models/perm/access"
 	repo_model "code.gitea.io/gitea/models/repo"
-	"code.gitea.io/gitea/models/unit"
 	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/container"
-	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
@@ -80,53 +76,6 @@ func init() {
 	db.RegisterModel(new(Notification))
 }
 
-// FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored.
-type FindNotificationOptions struct {
-	db.ListOptions
-	UserID            int64
-	RepoID            int64
-	IssueID           int64
-	Status            []NotificationStatus
-	Source            []NotificationSource
-	UpdatedAfterUnix  int64
-	UpdatedBeforeUnix int64
-}
-
-// ToCond will convert each condition into a xorm-Cond
-func (opts FindNotificationOptions) ToConds() builder.Cond {
-	cond := builder.NewCond()
-	if opts.UserID != 0 {
-		cond = cond.And(builder.Eq{"notification.user_id": opts.UserID})
-	}
-	if opts.RepoID != 0 {
-		cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID})
-	}
-	if opts.IssueID != 0 {
-		cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID})
-	}
-	if len(opts.Status) > 0 {
-		if len(opts.Status) == 1 {
-			cond = cond.And(builder.Eq{"notification.status": opts.Status[0]})
-		} else {
-			cond = cond.And(builder.In("notification.status", opts.Status))
-		}
-	}
-	if len(opts.Source) > 0 {
-		cond = cond.And(builder.In("notification.source", opts.Source))
-	}
-	if opts.UpdatedAfterUnix != 0 {
-		cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix})
-	}
-	if opts.UpdatedBeforeUnix != 0 {
-		cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix})
-	}
-	return cond
-}
-
-func (opts FindNotificationOptions) ToOrders() string {
-	return "notification.updated_unix DESC"
-}
-
 // CreateRepoTransferNotification creates  notification for the user a repository was transferred to
 func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) error {
 	return db.WithTx(ctx, func(ctx context.Context) error {
@@ -160,118 +109,6 @@ func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_mo
 	})
 }
 
-// CreateOrUpdateIssueNotifications creates an issue notification
-// for each watcher, or updates it if already exists
-// receiverID > 0 just send to receiver, else send to all watcher
-func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
-	ctx, committer, err := db.TxContext(ctx)
-	if err != nil {
-		return err
-	}
-	defer committer.Close()
-
-	if err := createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID); err != nil {
-		return err
-	}
-
-	return committer.Commit()
-}
-
-func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
-	// init
-	var toNotify container.Set[int64]
-	notifications, err := db.Find[Notification](ctx, FindNotificationOptions{
-		IssueID: issueID,
-	})
-	if err != nil {
-		return err
-	}
-
-	issue, err := issues_model.GetIssueByID(ctx, issueID)
-	if err != nil {
-		return err
-	}
-
-	if receiverID > 0 {
-		toNotify = make(container.Set[int64], 1)
-		toNotify.Add(receiverID)
-	} else {
-		toNotify = make(container.Set[int64], 32)
-		issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true)
-		if err != nil {
-			return err
-		}
-		toNotify.AddMultiple(issueWatches...)
-		if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) {
-			repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID)
-			if err != nil {
-				return err
-			}
-			toNotify.AddMultiple(repoWatches...)
-		}
-		issueParticipants, err := issue.GetParticipantIDsByIssue(ctx)
-		if err != nil {
-			return err
-		}
-		toNotify.AddMultiple(issueParticipants...)
-
-		// dont notify user who cause notification
-		delete(toNotify, notificationAuthorID)
-		// explicit unwatch on issue
-		issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false)
-		if err != nil {
-			return err
-		}
-		for _, id := range issueUnWatches {
-			toNotify.Remove(id)
-		}
-
-		// Remove users who have the notification author blocked.
-		blockedAuthorIDs, err := user_model.ListBlockedByUsersID(ctx, notificationAuthorID)
-		if err != nil {
-			return err
-		}
-		for _, id := range blockedAuthorIDs {
-			toNotify.Remove(id)
-		}
-	}
-
-	err = issue.LoadRepo(ctx)
-	if err != nil {
-		return err
-	}
-
-	// notify
-	for userID := range toNotify {
-		issue.Repo.Units = nil
-		user, err := user_model.GetUserByID(ctx, userID)
-		if err != nil {
-			if user_model.IsErrUserNotExist(err) {
-				continue
-			}
-
-			return err
-		}
-		if issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) {
-			continue
-		}
-		if !issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) {
-			continue
-		}
-
-		if notificationExists(notifications, issue.ID, userID) {
-			if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil {
-				return err
-			}
-			continue
-		}
-		if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
 func createIssueNotification(ctx context.Context, userID int64, issue *issues_model.Issue, commentID, updatedByID int64) error {
 	notification := &Notification{
 		UserID:    userID,
@@ -459,309 +296,6 @@ func GetUIDsAndNotificationCounts(ctx context.Context, since, until timeutil.Tim
 	return res, db.GetEngine(ctx).SQL(sql, since, until, NotificationStatusUnread).Find(&res)
 }
 
-// NotificationList contains a list of notifications
-type NotificationList []*Notification
-
-// LoadAttributes load Repo Issue User and Comment if not loaded
-func (nl NotificationList) LoadAttributes(ctx context.Context) error {
-	if _, _, err := nl.LoadRepos(ctx); err != nil {
-		return err
-	}
-	if _, err := nl.LoadIssues(ctx); err != nil {
-		return err
-	}
-	if _, err := nl.LoadUsers(ctx); err != nil {
-		return err
-	}
-	if _, err := nl.LoadComments(ctx); err != nil {
-		return err
-	}
-	return nil
-}
-
-func (nl NotificationList) getPendingRepoIDs() []int64 {
-	ids := make(container.Set[int64], len(nl))
-	for _, notification := range nl {
-		if notification.Repository != nil {
-			continue
-		}
-		ids.Add(notification.RepoID)
-	}
-	return ids.Values()
-}
-
-// LoadRepos loads repositories from database
-func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.RepositoryList, []int, error) {
-	if len(nl) == 0 {
-		return repo_model.RepositoryList{}, []int{}, nil
-	}
-
-	repoIDs := nl.getPendingRepoIDs()
-	repos := make(map[int64]*repo_model.Repository, len(repoIDs))
-	left := len(repoIDs)
-	for left > 0 {
-		limit := db.DefaultMaxInSize
-		if left < limit {
-			limit = left
-		}
-		rows, err := db.GetEngine(ctx).
-			In("id", repoIDs[:limit]).
-			Rows(new(repo_model.Repository))
-		if err != nil {
-			return nil, nil, err
-		}
-
-		for rows.Next() {
-			var repo repo_model.Repository
-			err = rows.Scan(&repo)
-			if err != nil {
-				rows.Close()
-				return nil, nil, err
-			}
-
-			repos[repo.ID] = &repo
-		}
-		_ = rows.Close()
-
-		left -= limit
-		repoIDs = repoIDs[limit:]
-	}
-
-	failed := []int{}
-
-	reposList := make(repo_model.RepositoryList, 0, len(repoIDs))
-	for i, notification := range nl {
-		if notification.Repository == nil {
-			notification.Repository = repos[notification.RepoID]
-		}
-		if notification.Repository == nil {
-			log.Error("Notification[%d]: RepoID: %d not found", notification.ID, notification.RepoID)
-			failed = append(failed, i)
-			continue
-		}
-		var found bool
-		for _, r := range reposList {
-			if r.ID == notification.RepoID {
-				found = true
-				break
-			}
-		}
-		if !found {
-			reposList = append(reposList, notification.Repository)
-		}
-	}
-	return reposList, failed, nil
-}
-
-func (nl NotificationList) getPendingIssueIDs() []int64 {
-	ids := make(container.Set[int64], len(nl))
-	for _, notification := range nl {
-		if notification.Issue != nil {
-			continue
-		}
-		ids.Add(notification.IssueID)
-	}
-	return ids.Values()
-}
-
-// LoadIssues loads issues from database
-func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) {
-	if len(nl) == 0 {
-		return []int{}, nil
-	}
-
-	issueIDs := nl.getPendingIssueIDs()
-	issues := make(map[int64]*issues_model.Issue, len(issueIDs))
-	left := len(issueIDs)
-	for left > 0 {
-		limit := db.DefaultMaxInSize
-		if left < limit {
-			limit = left
-		}
-		rows, err := db.GetEngine(ctx).
-			In("id", issueIDs[:limit]).
-			Rows(new(issues_model.Issue))
-		if err != nil {
-			return nil, err
-		}
-
-		for rows.Next() {
-			var issue issues_model.Issue
-			err = rows.Scan(&issue)
-			if err != nil {
-				rows.Close()
-				return nil, err
-			}
-
-			issues[issue.ID] = &issue
-		}
-		_ = rows.Close()
-
-		left -= limit
-		issueIDs = issueIDs[limit:]
-	}
-
-	failures := []int{}
-
-	for i, notification := range nl {
-		if notification.Issue == nil {
-			notification.Issue = issues[notification.IssueID]
-			if notification.Issue == nil {
-				if notification.IssueID != 0 {
-					log.Error("Notification[%d]: IssueID: %d Not Found", notification.ID, notification.IssueID)
-					failures = append(failures, i)
-				}
-				continue
-			}
-			notification.Issue.Repo = notification.Repository
-		}
-	}
-	return failures, nil
-}
-
-// Without returns the notification list without the failures
-func (nl NotificationList) Without(failures []int) NotificationList {
-	if len(failures) == 0 {
-		return nl
-	}
-	remaining := make([]*Notification, 0, len(nl))
-	last := -1
-	var i int
-	for _, i = range failures {
-		remaining = append(remaining, nl[last+1:i]...)
-		last = i
-	}
-	if len(nl) > i {
-		remaining = append(remaining, nl[i+1:]...)
-	}
-	return remaining
-}
-
-func (nl NotificationList) getPendingCommentIDs() []int64 {
-	ids := make(container.Set[int64], len(nl))
-	for _, notification := range nl {
-		if notification.CommentID == 0 || notification.Comment != nil {
-			continue
-		}
-		ids.Add(notification.CommentID)
-	}
-	return ids.Values()
-}
-
-func (nl NotificationList) getUserIDs() []int64 {
-	ids := make(container.Set[int64], len(nl))
-	for _, notification := range nl {
-		if notification.UserID == 0 || notification.User != nil {
-			continue
-		}
-		ids.Add(notification.UserID)
-	}
-	return ids.Values()
-}
-
-// LoadUsers loads users from database
-func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) {
-	if len(nl) == 0 {
-		return []int{}, nil
-	}
-
-	userIDs := nl.getUserIDs()
-	users := make(map[int64]*user_model.User, len(userIDs))
-	left := len(userIDs)
-	for left > 0 {
-		limit := db.DefaultMaxInSize
-		if left < limit {
-			limit = left
-		}
-		rows, err := db.GetEngine(ctx).
-			In("id", userIDs[:limit]).
-			Rows(new(user_model.User))
-		if err != nil {
-			return nil, err
-		}
-
-		for rows.Next() {
-			var user user_model.User
-			err = rows.Scan(&user)
-			if err != nil {
-				rows.Close()
-				return nil, err
-			}
-
-			users[user.ID] = &user
-		}
-		_ = rows.Close()
-
-		left -= limit
-		userIDs = userIDs[limit:]
-	}
-
-	failures := []int{}
-	for i, notification := range nl {
-		if notification.UserID > 0 && notification.User == nil && users[notification.UserID] != nil {
-			notification.User = users[notification.UserID]
-			if notification.User == nil {
-				log.Error("Notification[%d]: UserID[%d] failed to load", notification.ID, notification.UserID)
-				failures = append(failures, i)
-				continue
-			}
-		}
-	}
-	return failures, nil
-}
-
-// LoadComments loads comments from database
-func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) {
-	if len(nl) == 0 {
-		return []int{}, nil
-	}
-
-	commentIDs := nl.getPendingCommentIDs()
-	comments := make(map[int64]*issues_model.Comment, len(commentIDs))
-	left := len(commentIDs)
-	for left > 0 {
-		limit := db.DefaultMaxInSize
-		if left < limit {
-			limit = left
-		}
-		rows, err := db.GetEngine(ctx).
-			In("id", commentIDs[:limit]).
-			Rows(new(issues_model.Comment))
-		if err != nil {
-			return nil, err
-		}
-
-		for rows.Next() {
-			var comment issues_model.Comment
-			err = rows.Scan(&comment)
-			if err != nil {
-				rows.Close()
-				return nil, err
-			}
-
-			comments[comment.ID] = &comment
-		}
-		_ = rows.Close()
-
-		left -= limit
-		commentIDs = commentIDs[limit:]
-	}
-
-	failures := []int{}
-	for i, notification := range nl {
-		if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil {
-			notification.Comment = comments[notification.CommentID]
-			if notification.Comment == nil {
-				log.Error("Notification[%d]: CommentID[%d] failed to load", notification.ID, notification.CommentID)
-				failures = append(failures, i)
-				continue
-			}
-			notification.Comment.Issue = notification.Issue
-		}
-	}
-	return failures, nil
-}
-
 // SetIssueReadBy sets issue to be read by given user.
 func SetIssueReadBy(ctx context.Context, issueID, userID int64) error {
 	if err := issues_model.UpdateIssueUserByRead(ctx, userID, issueID); err != nil {
diff --git a/models/activities/notification_list.go b/models/activities/notification_list.go
new file mode 100644
index 0000000000..a6140d6f73
--- /dev/null
+++ b/models/activities/notification_list.go
@@ -0,0 +1,480 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package activities
+
+import (
+	"context"
+
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	access_model "code.gitea.io/gitea/models/perm/access"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unit"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/container"
+	"code.gitea.io/gitea/modules/log"
+
+	"xorm.io/builder"
+)
+
+// FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored.
+type FindNotificationOptions struct {
+	db.ListOptions
+	UserID            int64
+	RepoID            int64
+	IssueID           int64
+	Status            []NotificationStatus
+	Source            []NotificationSource
+	UpdatedAfterUnix  int64
+	UpdatedBeforeUnix int64
+}
+
+// ToCond will convert each condition into a xorm-Cond
+func (opts FindNotificationOptions) ToConds() builder.Cond {
+	cond := builder.NewCond()
+	if opts.UserID != 0 {
+		cond = cond.And(builder.Eq{"notification.user_id": opts.UserID})
+	}
+	if opts.RepoID != 0 {
+		cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID})
+	}
+	if opts.IssueID != 0 {
+		cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID})
+	}
+	if len(opts.Status) > 0 {
+		if len(opts.Status) == 1 {
+			cond = cond.And(builder.Eq{"notification.status": opts.Status[0]})
+		} else {
+			cond = cond.And(builder.In("notification.status", opts.Status))
+		}
+	}
+	if len(opts.Source) > 0 {
+		cond = cond.And(builder.In("notification.source", opts.Source))
+	}
+	if opts.UpdatedAfterUnix != 0 {
+		cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix})
+	}
+	if opts.UpdatedBeforeUnix != 0 {
+		cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix})
+	}
+	return cond
+}
+
+func (opts FindNotificationOptions) ToOrders() string {
+	return "notification.updated_unix DESC"
+}
+
+// CreateOrUpdateIssueNotifications creates an issue notification
+// for each watcher, or updates it if already exists
+// receiverID > 0 just send to receiver, else send to all watcher
+func CreateOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
+	ctx, committer, err := db.TxContext(ctx)
+	if err != nil {
+		return err
+	}
+	defer committer.Close()
+
+	if err := createOrUpdateIssueNotifications(ctx, issueID, commentID, notificationAuthorID, receiverID); err != nil {
+		return err
+	}
+
+	return committer.Commit()
+}
+
+func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, notificationAuthorID, receiverID int64) error {
+	// init
+	var toNotify container.Set[int64]
+	notifications, err := db.Find[Notification](ctx, FindNotificationOptions{
+		IssueID: issueID,
+	})
+	if err != nil {
+		return err
+	}
+
+	issue, err := issues_model.GetIssueByID(ctx, issueID)
+	if err != nil {
+		return err
+	}
+
+	if receiverID > 0 {
+		toNotify = make(container.Set[int64], 1)
+		toNotify.Add(receiverID)
+	} else {
+		toNotify = make(container.Set[int64], 32)
+		issueWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, true)
+		if err != nil {
+			return err
+		}
+		toNotify.AddMultiple(issueWatches...)
+		if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) {
+			repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID)
+			if err != nil {
+				return err
+			}
+			toNotify.AddMultiple(repoWatches...)
+		}
+		issueParticipants, err := issue.GetParticipantIDsByIssue(ctx)
+		if err != nil {
+			return err
+		}
+		toNotify.AddMultiple(issueParticipants...)
+
+		// dont notify user who cause notification
+		delete(toNotify, notificationAuthorID)
+		// explicit unwatch on issue
+		issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false)
+		if err != nil {
+			return err
+		}
+		for _, id := range issueUnWatches {
+			toNotify.Remove(id)
+		}
+		// Remove users who have the notification author blocked.
+		blockedAuthorIDs, err := user_model.ListBlockedByUsersID(ctx, notificationAuthorID)
+		if err != nil {
+			return err
+		}
+		for _, id := range blockedAuthorIDs {
+			toNotify.Remove(id)
+		}
+	}
+
+	err = issue.LoadRepo(ctx)
+	if err != nil {
+		return err
+	}
+
+	// notify
+	for userID := range toNotify {
+		issue.Repo.Units = nil
+		user, err := user_model.GetUserByID(ctx, userID)
+		if err != nil {
+			if user_model.IsErrUserNotExist(err) {
+				continue
+			}
+
+			return err
+		}
+		if issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypePullRequests) {
+			continue
+		}
+		if !issue.IsPull && !access_model.CheckRepoUnitUser(ctx, issue.Repo, user, unit.TypeIssues) {
+			continue
+		}
+
+		if notificationExists(notifications, issue.ID, userID) {
+			if err = updateIssueNotification(ctx, userID, issue.ID, commentID, notificationAuthorID); err != nil {
+				return err
+			}
+			continue
+		}
+		if err = createIssueNotification(ctx, userID, issue, commentID, notificationAuthorID); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// NotificationList contains a list of notifications
+type NotificationList []*Notification
+
+// LoadAttributes load Repo Issue User and Comment if not loaded
+func (nl NotificationList) LoadAttributes(ctx context.Context) error {
+	if _, _, err := nl.LoadRepos(ctx); err != nil {
+		return err
+	}
+	if _, err := nl.LoadIssues(ctx); err != nil {
+		return err
+	}
+	if _, err := nl.LoadUsers(ctx); err != nil {
+		return err
+	}
+	if _, err := nl.LoadComments(ctx); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (nl NotificationList) getPendingRepoIDs() []int64 {
+	ids := make(container.Set[int64], len(nl))
+	for _, notification := range nl {
+		if notification.Repository != nil {
+			continue
+		}
+		ids.Add(notification.RepoID)
+	}
+	return ids.Values()
+}
+
+// LoadRepos loads repositories from database
+func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.RepositoryList, []int, error) {
+	if len(nl) == 0 {
+		return repo_model.RepositoryList{}, []int{}, nil
+	}
+
+	repoIDs := nl.getPendingRepoIDs()
+	repos := make(map[int64]*repo_model.Repository, len(repoIDs))
+	left := len(repoIDs)
+	for left > 0 {
+		limit := db.DefaultMaxInSize
+		if left < limit {
+			limit = left
+		}
+		rows, err := db.GetEngine(ctx).
+			In("id", repoIDs[:limit]).
+			Rows(new(repo_model.Repository))
+		if err != nil {
+			return nil, nil, err
+		}
+
+		for rows.Next() {
+			var repo repo_model.Repository
+			err = rows.Scan(&repo)
+			if err != nil {
+				rows.Close()
+				return nil, nil, err
+			}
+
+			repos[repo.ID] = &repo
+		}
+		_ = rows.Close()
+
+		left -= limit
+		repoIDs = repoIDs[limit:]
+	}
+
+	failed := []int{}
+
+	reposList := make(repo_model.RepositoryList, 0, len(repoIDs))
+	for i, notification := range nl {
+		if notification.Repository == nil {
+			notification.Repository = repos[notification.RepoID]
+		}
+		if notification.Repository == nil {
+			log.Error("Notification[%d]: RepoID: %d not found", notification.ID, notification.RepoID)
+			failed = append(failed, i)
+			continue
+		}
+		var found bool
+		for _, r := range reposList {
+			if r.ID == notification.RepoID {
+				found = true
+				break
+			}
+		}
+		if !found {
+			reposList = append(reposList, notification.Repository)
+		}
+	}
+	return reposList, failed, nil
+}
+
+func (nl NotificationList) getPendingIssueIDs() []int64 {
+	ids := make(container.Set[int64], len(nl))
+	for _, notification := range nl {
+		if notification.Issue != nil {
+			continue
+		}
+		ids.Add(notification.IssueID)
+	}
+	return ids.Values()
+}
+
+// LoadIssues loads issues from database
+func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) {
+	if len(nl) == 0 {
+		return []int{}, nil
+	}
+
+	issueIDs := nl.getPendingIssueIDs()
+	issues := make(map[int64]*issues_model.Issue, len(issueIDs))
+	left := len(issueIDs)
+	for left > 0 {
+		limit := db.DefaultMaxInSize
+		if left < limit {
+			limit = left
+		}
+		rows, err := db.GetEngine(ctx).
+			In("id", issueIDs[:limit]).
+			Rows(new(issues_model.Issue))
+		if err != nil {
+			return nil, err
+		}
+
+		for rows.Next() {
+			var issue issues_model.Issue
+			err = rows.Scan(&issue)
+			if err != nil {
+				rows.Close()
+				return nil, err
+			}
+
+			issues[issue.ID] = &issue
+		}
+		_ = rows.Close()
+
+		left -= limit
+		issueIDs = issueIDs[limit:]
+	}
+
+	failures := []int{}
+
+	for i, notification := range nl {
+		if notification.Issue == nil {
+			notification.Issue = issues[notification.IssueID]
+			if notification.Issue == nil {
+				if notification.IssueID != 0 {
+					log.Error("Notification[%d]: IssueID: %d Not Found", notification.ID, notification.IssueID)
+					failures = append(failures, i)
+				}
+				continue
+			}
+			notification.Issue.Repo = notification.Repository
+		}
+	}
+	return failures, nil
+}
+
+// Without returns the notification list without the failures
+func (nl NotificationList) Without(failures []int) NotificationList {
+	if len(failures) == 0 {
+		return nl
+	}
+	remaining := make([]*Notification, 0, len(nl))
+	last := -1
+	var i int
+	for _, i = range failures {
+		remaining = append(remaining, nl[last+1:i]...)
+		last = i
+	}
+	if len(nl) > i {
+		remaining = append(remaining, nl[i+1:]...)
+	}
+	return remaining
+}
+
+func (nl NotificationList) getPendingCommentIDs() []int64 {
+	ids := make(container.Set[int64], len(nl))
+	for _, notification := range nl {
+		if notification.CommentID == 0 || notification.Comment != nil {
+			continue
+		}
+		ids.Add(notification.CommentID)
+	}
+	return ids.Values()
+}
+
+func (nl NotificationList) getUserIDs() []int64 {
+	ids := make(container.Set[int64], len(nl))
+	for _, notification := range nl {
+		if notification.UserID == 0 || notification.User != nil {
+			continue
+		}
+		ids.Add(notification.UserID)
+	}
+	return ids.Values()
+}
+
+// LoadUsers loads users from database
+func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) {
+	if len(nl) == 0 {
+		return []int{}, nil
+	}
+
+	userIDs := nl.getUserIDs()
+	users := make(map[int64]*user_model.User, len(userIDs))
+	left := len(userIDs)
+	for left > 0 {
+		limit := db.DefaultMaxInSize
+		if left < limit {
+			limit = left
+		}
+		rows, err := db.GetEngine(ctx).
+			In("id", userIDs[:limit]).
+			Rows(new(user_model.User))
+		if err != nil {
+			return nil, err
+		}
+
+		for rows.Next() {
+			var user user_model.User
+			err = rows.Scan(&user)
+			if err != nil {
+				rows.Close()
+				return nil, err
+			}
+
+			users[user.ID] = &user
+		}
+		_ = rows.Close()
+
+		left -= limit
+		userIDs = userIDs[limit:]
+	}
+
+	failures := []int{}
+	for i, notification := range nl {
+		if notification.UserID > 0 && notification.User == nil && users[notification.UserID] != nil {
+			notification.User = users[notification.UserID]
+			if notification.User == nil {
+				log.Error("Notification[%d]: UserID[%d] failed to load", notification.ID, notification.UserID)
+				failures = append(failures, i)
+				continue
+			}
+		}
+	}
+	return failures, nil
+}
+
+// LoadComments loads comments from database
+func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) {
+	if len(nl) == 0 {
+		return []int{}, nil
+	}
+
+	commentIDs := nl.getPendingCommentIDs()
+	comments := make(map[int64]*issues_model.Comment, len(commentIDs))
+	left := len(commentIDs)
+	for left > 0 {
+		limit := db.DefaultMaxInSize
+		if left < limit {
+			limit = left
+		}
+		rows, err := db.GetEngine(ctx).
+			In("id", commentIDs[:limit]).
+			Rows(new(issues_model.Comment))
+		if err != nil {
+			return nil, err
+		}
+
+		for rows.Next() {
+			var comment issues_model.Comment
+			err = rows.Scan(&comment)
+			if err != nil {
+				rows.Close()
+				return nil, err
+			}
+
+			comments[comment.ID] = &comment
+		}
+		_ = rows.Close()
+
+		left -= limit
+		commentIDs = commentIDs[limit:]
+	}
+
+	failures := []int{}
+	for i, notification := range nl {
+		if notification.CommentID > 0 && notification.Comment == nil && comments[notification.CommentID] != nil {
+			notification.Comment = comments[notification.CommentID]
+			if notification.Comment == nil {
+				log.Error("Notification[%d]: CommentID[%d] failed to load", notification.ID, notification.CommentID)
+				failures = append(failures, i)
+				continue
+			}
+			notification.Comment.Issue = notification.Issue
+		}
+	}
+	return failures, nil
+}