diff --git a/models/activities/action.go b/models/activities/action.go index c16c49c0ac..1993364fb6 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -442,6 +442,7 @@ type GetFeedsOptions struct { OnlyPerformedBy bool // only actions performed by requested user IncludeDeleted bool // include deleted actions Date string // the day we want activity for: YYYY-MM-DD + DontCount bool // do counting in GetFeeds } // ActivityReadable return whether doer can read activities of user diff --git a/models/activities/action_list.go b/models/activities/action_list.go index f7ea48f03e..6789ebcb99 100644 --- a/models/activities/action_list.go +++ b/models/activities/action_list.go @@ -243,7 +243,11 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err sess := db.GetEngine(ctx).Where(cond) sess = db.SetSessionPagination(sess, &opts) - count, err = sess.Desc("`action`.created_unix").FindAndCount(&actions) + if opts.DontCount { + err = sess.Desc("`action`.created_unix").Find(&actions) + } else { + count, err = sess.Desc("`action`.created_unix").FindAndCount(&actions) + } if err != nil { return nil, 0, fmt.Errorf("FindAndCount: %w", err) } @@ -257,11 +261,13 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err return nil, 0, fmt.Errorf("Find(actionsIDs): %w", err) } - count, err = db.GetEngine(ctx).Where(cond). - Table("action"). - Cols("`action`.id").Count() - if err != nil { - return nil, 0, fmt.Errorf("Count: %w", err) + if !opts.DontCount { + count, err = db.GetEngine(ctx).Where(cond). + Table("action"). + Cols("`action`.id").Count() + if err != nil { + return nil, 0, fmt.Errorf("Count: %w", err) + } } if err := db.GetEngine(ctx).In("`action`.id", actionIDs).Desc("`action`.created_unix").Find(&actions); err != nil { @@ -275,3 +281,9 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err return actions, count, nil } + +func CountUserFeeds(ctx context.Context, userID int64) (int64, error) { + return db.GetEngine(ctx).Where("user_id = ?", userID). + And("is_deleted = ?", false). + Count(&Action{}) +} diff --git a/models/activities/user_heatmap.go b/models/activities/user_heatmap.go index 1f8f0f590e..5326986d62 100644 --- a/models/activities/user_heatmap.go +++ b/models/activities/user_heatmap.go @@ -19,17 +19,7 @@ type UserHeatmapData struct { Contributions int64 `json:"contributions"` } -// GetUserHeatmapDataByUser returns an array of UserHeatmapData -func GetUserHeatmapDataByUser(ctx context.Context, user, doer *user_model.User) ([]*UserHeatmapData, error) { - return getUserHeatmapData(ctx, user, nil, doer) -} - -// GetUserHeatmapDataByUserTeam returns an array of UserHeatmapData -func GetUserHeatmapDataByUserTeam(ctx context.Context, user *user_model.User, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) { - return getUserHeatmapData(ctx, user, team, doer) -} - -func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) { +func GetUserHeatmapData(ctx context.Context, user *user_model.User, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) { hdata := make([]*UserHeatmapData, 0) if !ActivityReadable(user, doer) { diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index 757a548518..9e6bf769cd 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -160,7 +160,7 @@ func GetUserHeatmapData(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - heatmap, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer) + heatmap, err := feed_service.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer) if err != nil { ctx.APIErrorInternal(err) return diff --git a/routers/web/user/home.go b/routers/web/user/home.go index dc78950cf2..dc36c70b85 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -109,7 +109,7 @@ func Dashboard(ctx *context.Context) { } if setting.Service.EnableUserHeatmap { - data, err := activities_model.GetUserHeatmapDataByUserTeam(ctx, ctxUser, ctx.Org.Team, ctx.Doer) + data, err := feed_service.GetUserHeatmapDataByUserTeam(ctx, ctxUser, ctx.Org.Team, ctx.Doer) if err != nil { ctx.ServerError("GetUserHeatmapDataByUserTeam", err) return diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index 39f066a53c..b4eed17547 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -65,7 +65,7 @@ func userProfile(ctx *context.Context) { // prepare heatmap data if setting.Service.EnableUserHeatmap { - data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer) + data, err := feed_service.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer) if err != nil { ctx.ServerError("GetUserHeatmapDataByUser", err) return diff --git a/services/feed/feed.go b/services/feed/feed.go index a1c327fb51..61dac3f33a 100644 --- a/services/feed/feed.go +++ b/services/feed/feed.go @@ -13,12 +13,27 @@ import ( 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/cache" "code.gitea.io/gitea/modules/setting" ) +func userFeedCacheKey(userID int64) string { + return fmt.Sprintf("user_feed_%d", userID) +} + // GetFeeds returns actions according to the provided options func GetFeeds(ctx context.Context, opts activities_model.GetFeedsOptions) (activities_model.ActionList, int64, error) { - return activities_model.GetFeeds(ctx, opts) + opts.DontCount = opts.Actor != nil && opts.RequestedUser != nil && (opts.Actor.IsAdmin || opts.Actor.ID == opts.RequestedUser.ID) + results, cnt, err := activities_model.GetFeeds(ctx, opts) + if err != nil { + return nil, 0, err + } + if opts.DontCount { + cnt, err = cache.GetInt64(userFeedCacheKey(opts.Actor.ID), func() (int64, error) { + return activities_model.CountUserFeeds(ctx, opts.Actor.ID) + }) + } + return results, cnt, err } // notifyWatchers creates batch of actions for every watcher. @@ -68,6 +83,13 @@ func notifyWatchers(ctx context.Context, act *activities_model.Action, watchers if err := db.Insert(ctx, act); err != nil { return fmt.Errorf("insert new action: %w", err) } + + total, err := activities_model.CountUserFeeds(ctx, act.UserID) + if err != nil { + return fmt.Errorf("count user feeds: %w", err) + } + + cache.GetCache().Put(userFeedCacheKey(act.UserID), fmt.Sprintf("%d", total), setting.CacheService.TTLSeconds()) } return nil diff --git a/services/feed/heatmap.go b/services/feed/heatmap.go new file mode 100644 index 0000000000..68c263d9f2 --- /dev/null +++ b/services/feed/heatmap.go @@ -0,0 +1,22 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package feed + +import ( + "context" + + activities_model "code.gitea.io/gitea/models/activities" + "code.gitea.io/gitea/models/organization" + user_model "code.gitea.io/gitea/models/user" +) + +// GetUserHeatmapDataByUser returns an array of UserHeatmapData +func GetUserHeatmapDataByUser(ctx context.Context, user, doer *user_model.User) ([]*activities_model.UserHeatmapData, error) { + return activities_model.GetUserHeatmapData(ctx, user, nil, doer) +} + +// GetUserHeatmapDataByUserTeam returns an array of UserHeatmapData +func GetUserHeatmapDataByUserTeam(ctx context.Context, user *user_model.User, team *organization.Team, doer *user_model.User) ([]*activities_model.UserHeatmapData, error) { + return activities_model.GetUserHeatmapData(ctx, user, team, doer) +}