From 34556a0ebdbcb0162552e4f21efb028d66662481 Mon Sep 17 00:00:00 2001 From: Christian Carlsson Date: Wed, 27 Nov 2024 22:25:28 +0000 Subject: [PATCH] feat: notifications (#109) --- database/migrations/015_notifications.up.sql | 13 + protobufs/api/v1/users.proto | 29 + server/bus/events/events.go | 5 +- server/bus/handlers/handlers.go | 45 +- server/bus/handlers/registry.go | 6 +- server/bus/module.go | 1 + server/bus/payloads/payloads.go | 4 + server/pkg/orm/boil_table_names.go | 2 + server/pkg/orm/boil_types.go | 40 + server/pkg/orm/notifications.go | 1217 +++++++++++++++++ server/pkg/orm/routines.go | 21 - server/pkg/orm/users.go | 190 +++ .../pb/api/v1/apiv1connect/users.connect.go | 56 +- server/pkg/pb/api/v1/users.pb.go | 548 ++++++-- server/pkg/repo/helpers.go | 2 +- server/pkg/repo/repo.go | 139 ++ server/rpc/v1/parser.go | 66 + server/rpc/v1/user.go | 77 ++ server/rpc/v1/workout.go | 15 +- web/src/proto/api/v1/users_pb.ts | 114 +- web/src/router/router.ts | 8 + web/src/ui/components/NavigationMobile.vue | 34 +- .../components/NotificationWorkoutComment.vue | 36 + web/src/ui/exercises/ListExercises.vue | 2 +- .../ui/notifications/ListNotifications.vue | 51 + 25 files changed, 2553 insertions(+), 168 deletions(-) create mode 100644 database/migrations/015_notifications.up.sql create mode 100644 server/pkg/orm/notifications.go create mode 100644 web/src/ui/components/NotificationWorkoutComment.vue create mode 100644 web/src/ui/notifications/ListNotifications.vue diff --git a/database/migrations/015_notifications.up.sql b/database/migrations/015_notifications.up.sql new file mode 100644 index 00000000..6b35dc69 --- /dev/null +++ b/database/migrations/015_notifications.up.sql @@ -0,0 +1,13 @@ +CREATE TYPE getstronger.notification_type AS ENUM ('Follow', 'WorkoutComment'); + +CREATE TABLE getstronger.notifications +( + id UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES getstronger.users (id), + "type" getstronger.notification_type NOT NULL, + payload JSONB NOT NULL, + read_at TIMESTAMP NULL, + created_at TIMESTAMP NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC') +); + +CREATE INDEX idx_notifications_user_id_created_at ON getstronger.notifications (user_id, read_at); diff --git a/protobufs/api/v1/users.proto b/protobufs/api/v1/users.proto index 50a1510a..3c85110c 100644 --- a/protobufs/api/v1/users.proto +++ b/protobufs/api/v1/users.proto @@ -4,6 +4,9 @@ package api.v1; import "api/v1/options.proto"; import "api/v1/shared.proto"; +import "api/v1/workouts.proto"; + +import "google/protobuf/timestamp.proto"; import "buf/validate/validate.proto"; @@ -26,6 +29,9 @@ service UserService { rpc Search(SearchRequest) returns (SearchResponse) { option (auth) = true; } + rpc ListNotifications(ListNotificationsRequest) returns (ListNotificationsResponse) { + option (auth) = true; + } } message GetUserRequest { @@ -67,3 +73,26 @@ message SearchResponse { repeated User users = 1; PaginationResponse pagination = 2; } + +message ListNotificationsRequest { + bool only_unread = 1; + PaginationRequest pagination = 2 [(buf.validate.field).required = true]; +} +message ListNotificationsResponse { + repeated Notification notifications = 1; + PaginationResponse pagination = 2; +} + +message Notification { + message WorkoutComment { + User actor = 1; + Workout workout = 2; + } + + string id = 1; + // DEBT: This should be a timestamp but the client is not able to parse it. + int64 notified_at_unix = 2; + oneof type { + WorkoutComment workout_comment = 3; + } +} diff --git a/server/bus/events/events.go b/server/bus/events/events.go index 025ee46f..ffcc78b8 100644 --- a/server/bus/events/events.go +++ b/server/bus/events/events.go @@ -1,3 +1,6 @@ package events -const RequestTraced = "request:traced" +const ( + RequestTraced = "request:traced" + WorkoutCommentPosted = "workout_comment:posted" +) diff --git a/server/bus/handlers/handlers.go b/server/bus/handlers/handlers.go index 78423d8b..3f9de977 100644 --- a/server/bus/handlers/handlers.go +++ b/server/bus/handlers/handlers.go @@ -7,6 +7,7 @@ import ( "go.uber.org/zap" "github.com/crlssn/getstronger/server/bus/payloads" + "github.com/crlssn/getstronger/server/pkg/orm" "github.com/crlssn/getstronger/server/pkg/repo" ) @@ -14,7 +15,10 @@ type Handler interface { HandlePayload(payload any) } -var _ Handler = (*RequestTraced)(nil) +var ( + _ Handler = (*RequestTraced)(nil) + _ Handler = (*WorkoutCommentPosted)(nil) +) type RequestTraced struct { log *zap.Logger @@ -44,3 +48,42 @@ func (h *RequestTraced) HandlePayload(payload any) { h.log.Error("unexpected event type", zap.Any("event", payload)) } } + +type WorkoutCommentPosted struct { + log *zap.Logger + repo *repo.Repo +} + +func NewWorkoutCommentPosted(log *zap.Logger, repo *repo.Repo) *WorkoutCommentPosted { + return &WorkoutCommentPosted{log, repo} +} + +func (w *WorkoutCommentPosted) HandlePayload(payload any) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + switch t := payload.(type) { + case *payloads.WorkoutCommentPosted: + comment, err := w.repo.GetWorkoutComment(ctx, + repo.GetWorkoutCommentWithID(t.CommentID), + repo.GetWorkoutCommentWithWorkout(), + ) + if err != nil { + w.log.Error("get workout comment", zap.Error(err)) + return + } + + if err = w.repo.CreateNotification(ctx, repo.CreateNotificationParams{ + Type: orm.NotificationTypeWorkoutComment, + UserID: comment.R.Workout.UserID, + Payload: repo.NotificationPayload{ + ActorID: comment.UserID, + WorkoutID: comment.WorkoutID, + }, + }); err != nil { + w.log.Error("create notification", zap.Error(err)) + } + default: + w.log.Error("unexpected event type", zap.Any("event", payload)) + } +} diff --git a/server/bus/handlers/registry.go b/server/bus/handlers/registry.go index 0b9c0111..7f4aca51 100644 --- a/server/bus/handlers/registry.go +++ b/server/bus/handlers/registry.go @@ -13,13 +13,15 @@ type Registry struct { type RegistryParams struct { fx.In - RequestTraced *RequestTraced + RequestTraced *RequestTraced + WorkoutCommentPosted *WorkoutCommentPosted } func NewRegistry(p RegistryParams) *Registry { return &Registry{ handlers: map[string]Handler{ - events.RequestTraced: p.RequestTraced, + events.RequestTraced: p.RequestTraced, + events.WorkoutCommentPosted: p.WorkoutCommentPosted, }, } } diff --git a/server/bus/module.go b/server/bus/module.go index 7197b53a..60eebd4e 100644 --- a/server/bus/module.go +++ b/server/bus/module.go @@ -15,6 +15,7 @@ func Module() fx.Option { New, handlers.NewRegistry, handlers.NewRequestTraced, + handlers.NewWorkoutCommentPosted, ), fx.Invoke( func(lc fx.Lifecycle, bus *Bus, registry *handlers.Registry) { diff --git a/server/bus/payloads/payloads.go b/server/bus/payloads/payloads.go index 462fff2b..0bf3d00b 100644 --- a/server/bus/payloads/payloads.go +++ b/server/bus/payloads/payloads.go @@ -5,3 +5,7 @@ type RequestTraced struct { DurationMS int StatusCode int } + +type WorkoutCommentPosted struct { + CommentID string +} diff --git a/server/pkg/orm/boil_table_names.go b/server/pkg/orm/boil_table_names.go index e4a7d00b..f29a83c8 100644 --- a/server/pkg/orm/boil_table_names.go +++ b/server/pkg/orm/boil_table_names.go @@ -8,6 +8,7 @@ var TableNames = struct { Exercises string ExercisesRoutines string Followers string + Notifications string Routines string Sets string Traces string @@ -19,6 +20,7 @@ var TableNames = struct { Exercises: "exercises", ExercisesRoutines: "exercises_routines", Followers: "followers", + Notifications: "notifications", Routines: "routines", Sets: "sets", Traces: "traces", diff --git a/server/pkg/orm/boil_types.go b/server/pkg/orm/boil_types.go index f6090689..4683fbd5 100644 --- a/server/pkg/orm/boil_types.go +++ b/server/pkg/orm/boil_types.go @@ -50,3 +50,43 @@ func makeCacheKey(cols boil.Columns, nzDefaults []string) string { strmangle.PutBuffer(buf) return str } + +type NotificationType string + +// Enum values for NotificationType +const ( + NotificationTypeFollow NotificationType = "Follow" + NotificationTypeWorkoutComment NotificationType = "WorkoutComment" +) + +func AllNotificationType() []NotificationType { + return []NotificationType{ + NotificationTypeFollow, + NotificationTypeWorkoutComment, + } +} + +func (e NotificationType) IsValid() error { + switch e { + case NotificationTypeFollow, NotificationTypeWorkoutComment: + return nil + default: + return errors.New("enum is not valid") + } +} + +func (e NotificationType) String() string { + return string(e) +} + +func (e NotificationType) Ordinal() int { + switch e { + case NotificationTypeFollow: + return 0 + case NotificationTypeWorkoutComment: + return 1 + + default: + panic(errors.New("enum is not valid")) + } +} diff --git a/server/pkg/orm/notifications.go b/server/pkg/orm/notifications.go new file mode 100644 index 00000000..d285cdc4 --- /dev/null +++ b/server/pkg/orm/notifications.go @@ -0,0 +1,1217 @@ +// Code generated by SQLBoiler 4.17.1 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package orm + +import ( + "context" + "database/sql" + "fmt" + "reflect" + "strconv" + "strings" + "sync" + "time" + + "github.com/friendsofgo/errors" + "github.com/volatiletech/null/v8" + "github.com/volatiletech/sqlboiler/v4/boil" + "github.com/volatiletech/sqlboiler/v4/queries" + "github.com/volatiletech/sqlboiler/v4/queries/qm" + "github.com/volatiletech/sqlboiler/v4/queries/qmhelper" + "github.com/volatiletech/sqlboiler/v4/types" + "github.com/volatiletech/strmangle" +) + +// Notification is an object representing the database table. +type Notification struct { + ID string `boil:"id" json:"id" toml:"id" yaml:"id"` + UserID string `boil:"user_id" json:"user_id" toml:"user_id" yaml:"user_id"` + Type NotificationType `boil:"type" json:"type" toml:"type" yaml:"type"` + Payload types.JSON `boil:"payload" json:"payload" toml:"payload" yaml:"payload"` + ReadAt null.Time `boil:"read_at" json:"read_at,omitempty" toml:"read_at" yaml:"read_at,omitempty"` + CreatedAt time.Time `boil:"created_at" json:"created_at" toml:"created_at" yaml:"created_at"` + + R *notificationR `boil:"-" json:"-" toml:"-" yaml:"-"` + L notificationL `boil:"-" json:"-" toml:"-" yaml:"-"` +} + +var NotificationColumns = struct { + ID string + UserID string + Type string + Payload string + ReadAt string + CreatedAt string +}{ + ID: "id", + UserID: "user_id", + Type: "type", + Payload: "payload", + ReadAt: "read_at", + CreatedAt: "created_at", +} + +var NotificationTableColumns = struct { + ID string + UserID string + Type string + Payload string + ReadAt string + CreatedAt string +}{ + ID: "notifications.id", + UserID: "notifications.user_id", + Type: "notifications.type", + Payload: "notifications.payload", + ReadAt: "notifications.read_at", + CreatedAt: "notifications.created_at", +} + +// Generated where + +type whereHelperNotificationType struct{ field string } + +func (w whereHelperNotificationType) EQ(x NotificationType) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.EQ, x) +} +func (w whereHelperNotificationType) NEQ(x NotificationType) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.NEQ, x) +} +func (w whereHelperNotificationType) LT(x NotificationType) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.LT, x) +} +func (w whereHelperNotificationType) LTE(x NotificationType) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.LTE, x) +} +func (w whereHelperNotificationType) GT(x NotificationType) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.GT, x) +} +func (w whereHelperNotificationType) GTE(x NotificationType) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.GTE, x) +} +func (w whereHelperNotificationType) IN(slice []NotificationType) qm.QueryMod { + values := make([]interface{}, 0, len(slice)) + for _, value := range slice { + values = append(values, value) + } + return qm.WhereIn(fmt.Sprintf("%s IN ?", w.field), values...) +} +func (w whereHelperNotificationType) NIN(slice []NotificationType) qm.QueryMod { + values := make([]interface{}, 0, len(slice)) + for _, value := range slice { + values = append(values, value) + } + return qm.WhereNotIn(fmt.Sprintf("%s NOT IN ?", w.field), values...) +} + +type whereHelpertypes_JSON struct{ field string } + +func (w whereHelpertypes_JSON) EQ(x types.JSON) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.EQ, x) +} +func (w whereHelpertypes_JSON) NEQ(x types.JSON) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.NEQ, x) +} +func (w whereHelpertypes_JSON) LT(x types.JSON) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.LT, x) +} +func (w whereHelpertypes_JSON) LTE(x types.JSON) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.LTE, x) +} +func (w whereHelpertypes_JSON) GT(x types.JSON) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.GT, x) +} +func (w whereHelpertypes_JSON) GTE(x types.JSON) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.GTE, x) +} + +var NotificationWhere = struct { + ID whereHelperstring + UserID whereHelperstring + Type whereHelperNotificationType + Payload whereHelpertypes_JSON + ReadAt whereHelpernull_Time + CreatedAt whereHelpertime_Time +}{ + ID: whereHelperstring{field: "\"getstronger\".\"notifications\".\"id\""}, + UserID: whereHelperstring{field: "\"getstronger\".\"notifications\".\"user_id\""}, + Type: whereHelperNotificationType{field: "\"getstronger\".\"notifications\".\"type\""}, + Payload: whereHelpertypes_JSON{field: "\"getstronger\".\"notifications\".\"payload\""}, + ReadAt: whereHelpernull_Time{field: "\"getstronger\".\"notifications\".\"read_at\""}, + CreatedAt: whereHelpertime_Time{field: "\"getstronger\".\"notifications\".\"created_at\""}, +} + +// NotificationRels is where relationship names are stored. +var NotificationRels = struct { + User string +}{ + User: "User", +} + +// notificationR is where relationships are stored. +type notificationR struct { + User *User `boil:"User" json:"User" toml:"User" yaml:"User"` +} + +// NewStruct creates a new relationship struct +func (*notificationR) NewStruct() *notificationR { + return ¬ificationR{} +} + +func (r *notificationR) GetUser() *User { + if r == nil { + return nil + } + return r.User +} + +// notificationL is where Load methods for each relationship are stored. +type notificationL struct{} + +var ( + notificationAllColumns = []string{"id", "user_id", "type", "payload", "read_at", "created_at"} + notificationColumnsWithoutDefault = []string{"user_id", "type", "payload"} + notificationColumnsWithDefault = []string{"id", "read_at", "created_at"} + notificationPrimaryKeyColumns = []string{"id"} + notificationGeneratedColumns = []string{} +) + +type ( + // NotificationSlice is an alias for a slice of pointers to Notification. + // This should almost always be used instead of []Notification. + NotificationSlice []*Notification + // NotificationHook is the signature for custom Notification hook methods + NotificationHook func(context.Context, boil.ContextExecutor, *Notification) error + + notificationQuery struct { + *queries.Query + } +) + +// Cache for insert, update and upsert +var ( + notificationType = reflect.TypeOf(&Notification{}) + notificationMapping = queries.MakeStructMapping(notificationType) + notificationPrimaryKeyMapping, _ = queries.BindMapping(notificationType, notificationMapping, notificationPrimaryKeyColumns) + notificationInsertCacheMut sync.RWMutex + notificationInsertCache = make(map[string]insertCache) + notificationUpdateCacheMut sync.RWMutex + notificationUpdateCache = make(map[string]updateCache) + notificationUpsertCacheMut sync.RWMutex + notificationUpsertCache = make(map[string]insertCache) +) + +var ( + // Force time package dependency for automated UpdatedAt/CreatedAt. + _ = time.Second + // Force qmhelper dependency for where clause generation (which doesn't + // always happen) + _ = qmhelper.Where +) + +var notificationAfterSelectMu sync.Mutex +var notificationAfterSelectHooks []NotificationHook + +var notificationBeforeInsertMu sync.Mutex +var notificationBeforeInsertHooks []NotificationHook +var notificationAfterInsertMu sync.Mutex +var notificationAfterInsertHooks []NotificationHook + +var notificationBeforeUpdateMu sync.Mutex +var notificationBeforeUpdateHooks []NotificationHook +var notificationAfterUpdateMu sync.Mutex +var notificationAfterUpdateHooks []NotificationHook + +var notificationBeforeDeleteMu sync.Mutex +var notificationBeforeDeleteHooks []NotificationHook +var notificationAfterDeleteMu sync.Mutex +var notificationAfterDeleteHooks []NotificationHook + +var notificationBeforeUpsertMu sync.Mutex +var notificationBeforeUpsertHooks []NotificationHook +var notificationAfterUpsertMu sync.Mutex +var notificationAfterUpsertHooks []NotificationHook + +// doAfterSelectHooks executes all "after Select" hooks. +func (o *Notification) doAfterSelectHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range notificationAfterSelectHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeInsertHooks executes all "before insert" hooks. +func (o *Notification) doBeforeInsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range notificationBeforeInsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterInsertHooks executes all "after Insert" hooks. +func (o *Notification) doAfterInsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range notificationAfterInsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeUpdateHooks executes all "before Update" hooks. +func (o *Notification) doBeforeUpdateHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range notificationBeforeUpdateHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterUpdateHooks executes all "after Update" hooks. +func (o *Notification) doAfterUpdateHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range notificationAfterUpdateHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeDeleteHooks executes all "before Delete" hooks. +func (o *Notification) doBeforeDeleteHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range notificationBeforeDeleteHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterDeleteHooks executes all "after Delete" hooks. +func (o *Notification) doAfterDeleteHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range notificationAfterDeleteHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeUpsertHooks executes all "before Upsert" hooks. +func (o *Notification) doBeforeUpsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range notificationBeforeUpsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterUpsertHooks executes all "after Upsert" hooks. +func (o *Notification) doAfterUpsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range notificationAfterUpsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// AddNotificationHook registers your hook function for all future operations. +func AddNotificationHook(hookPoint boil.HookPoint, notificationHook NotificationHook) { + switch hookPoint { + case boil.AfterSelectHook: + notificationAfterSelectMu.Lock() + notificationAfterSelectHooks = append(notificationAfterSelectHooks, notificationHook) + notificationAfterSelectMu.Unlock() + case boil.BeforeInsertHook: + notificationBeforeInsertMu.Lock() + notificationBeforeInsertHooks = append(notificationBeforeInsertHooks, notificationHook) + notificationBeforeInsertMu.Unlock() + case boil.AfterInsertHook: + notificationAfterInsertMu.Lock() + notificationAfterInsertHooks = append(notificationAfterInsertHooks, notificationHook) + notificationAfterInsertMu.Unlock() + case boil.BeforeUpdateHook: + notificationBeforeUpdateMu.Lock() + notificationBeforeUpdateHooks = append(notificationBeforeUpdateHooks, notificationHook) + notificationBeforeUpdateMu.Unlock() + case boil.AfterUpdateHook: + notificationAfterUpdateMu.Lock() + notificationAfterUpdateHooks = append(notificationAfterUpdateHooks, notificationHook) + notificationAfterUpdateMu.Unlock() + case boil.BeforeDeleteHook: + notificationBeforeDeleteMu.Lock() + notificationBeforeDeleteHooks = append(notificationBeforeDeleteHooks, notificationHook) + notificationBeforeDeleteMu.Unlock() + case boil.AfterDeleteHook: + notificationAfterDeleteMu.Lock() + notificationAfterDeleteHooks = append(notificationAfterDeleteHooks, notificationHook) + notificationAfterDeleteMu.Unlock() + case boil.BeforeUpsertHook: + notificationBeforeUpsertMu.Lock() + notificationBeforeUpsertHooks = append(notificationBeforeUpsertHooks, notificationHook) + notificationBeforeUpsertMu.Unlock() + case boil.AfterUpsertHook: + notificationAfterUpsertMu.Lock() + notificationAfterUpsertHooks = append(notificationAfterUpsertHooks, notificationHook) + notificationAfterUpsertMu.Unlock() + } +} + +// One returns a single notification record from the query. +func (q notificationQuery) One(ctx context.Context, exec boil.ContextExecutor) (*Notification, error) { + o := &Notification{} + + queries.SetLimit(q.Query, 1) + + err := q.Bind(ctx, exec, o) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, sql.ErrNoRows + } + return nil, errors.Wrap(err, "orm: failed to execute a one query for notifications") + } + + if err := o.doAfterSelectHooks(ctx, exec); err != nil { + return o, err + } + + return o, nil +} + +// All returns all Notification records from the query. +func (q notificationQuery) All(ctx context.Context, exec boil.ContextExecutor) (NotificationSlice, error) { + var o []*Notification + + err := q.Bind(ctx, exec, &o) + if err != nil { + return nil, errors.Wrap(err, "orm: failed to assign all query results to Notification slice") + } + + if len(notificationAfterSelectHooks) != 0 { + for _, obj := range o { + if err := obj.doAfterSelectHooks(ctx, exec); err != nil { + return o, err + } + } + } + + return o, nil +} + +// Count returns the count of all Notification records in the query. +func (q notificationQuery) Count(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + var count int64 + + queries.SetSelect(q.Query, nil) + queries.SetCount(q.Query) + + err := q.Query.QueryRowContext(ctx, exec).Scan(&count) + if err != nil { + return 0, errors.Wrap(err, "orm: failed to count notifications rows") + } + + return count, nil +} + +// Exists checks if the row exists in the table. +func (q notificationQuery) Exists(ctx context.Context, exec boil.ContextExecutor) (bool, error) { + var count int64 + + queries.SetSelect(q.Query, nil) + queries.SetCount(q.Query) + queries.SetLimit(q.Query, 1) + + err := q.Query.QueryRowContext(ctx, exec).Scan(&count) + if err != nil { + return false, errors.Wrap(err, "orm: failed to check if notifications exists") + } + + return count > 0, nil +} + +// User pointed to by the foreign key. +func (o *Notification) User(mods ...qm.QueryMod) userQuery { + queryMods := []qm.QueryMod{ + qm.Where("\"id\" = ?", o.UserID), + } + + queryMods = append(queryMods, mods...) + + return Users(queryMods...) +} + +// LoadUser allows an eager lookup of values, cached into the +// loaded structs of the objects. This is for an N-1 relationship. +func (notificationL) LoadUser(ctx context.Context, e boil.ContextExecutor, singular bool, maybeNotification interface{}, mods queries.Applicator) error { + var slice []*Notification + var object *Notification + + if singular { + var ok bool + object, ok = maybeNotification.(*Notification) + if !ok { + object = new(Notification) + ok = queries.SetFromEmbeddedStruct(&object, &maybeNotification) + if !ok { + return errors.New(fmt.Sprintf("failed to set %T from embedded struct %T", object, maybeNotification)) + } + } + } else { + s, ok := maybeNotification.(*[]*Notification) + if ok { + slice = *s + } else { + ok = queries.SetFromEmbeddedStruct(&slice, maybeNotification) + if !ok { + return errors.New(fmt.Sprintf("failed to set %T from embedded struct %T", slice, maybeNotification)) + } + } + } + + args := make(map[interface{}]struct{}) + if singular { + if object.R == nil { + object.R = ¬ificationR{} + } + args[object.UserID] = struct{}{} + + } else { + for _, obj := range slice { + if obj.R == nil { + obj.R = ¬ificationR{} + } + + args[obj.UserID] = struct{}{} + + } + } + + if len(args) == 0 { + return nil + } + + argsSlice := make([]interface{}, len(args)) + i := 0 + for arg := range args { + argsSlice[i] = arg + i++ + } + + query := NewQuery( + qm.From(`getstronger.users`), + qm.WhereIn(`getstronger.users.id in ?`, argsSlice...), + ) + if mods != nil { + mods.Apply(query) + } + + results, err := query.QueryContext(ctx, e) + if err != nil { + return errors.Wrap(err, "failed to eager load User") + } + + var resultSlice []*User + if err = queries.Bind(results, &resultSlice); err != nil { + return errors.Wrap(err, "failed to bind eager loaded slice User") + } + + if err = results.Close(); err != nil { + return errors.Wrap(err, "failed to close results of eager load for users") + } + if err = results.Err(); err != nil { + return errors.Wrap(err, "error occurred during iteration of eager loaded relations for users") + } + + if len(userAfterSelectHooks) != 0 { + for _, obj := range resultSlice { + if err := obj.doAfterSelectHooks(ctx, e); err != nil { + return err + } + } + } + + if len(resultSlice) == 0 { + return nil + } + + if singular { + foreign := resultSlice[0] + object.R.User = foreign + if foreign.R == nil { + foreign.R = &userR{} + } + foreign.R.Notifications = append(foreign.R.Notifications, object) + return nil + } + + for _, local := range slice { + for _, foreign := range resultSlice { + if local.UserID == foreign.ID { + local.R.User = foreign + if foreign.R == nil { + foreign.R = &userR{} + } + foreign.R.Notifications = append(foreign.R.Notifications, local) + break + } + } + } + + return nil +} + +// SetUser of the notification to the related item. +// Sets o.R.User to related. +// Adds o to related.R.Notifications. +func (o *Notification) SetUser(ctx context.Context, exec boil.ContextExecutor, insert bool, related *User) error { + var err error + if insert { + if err = related.Insert(ctx, exec, boil.Infer()); err != nil { + return errors.Wrap(err, "failed to insert into foreign table") + } + } + + updateQuery := fmt.Sprintf( + "UPDATE \"getstronger\".\"notifications\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 1, []string{"user_id"}), + strmangle.WhereClause("\"", "\"", 2, notificationPrimaryKeyColumns), + ) + values := []interface{}{related.ID, o.ID} + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, updateQuery) + fmt.Fprintln(writer, values) + } + if _, err = exec.ExecContext(ctx, updateQuery, values...); err != nil { + return errors.Wrap(err, "failed to update local table") + } + + o.UserID = related.ID + if o.R == nil { + o.R = ¬ificationR{ + User: related, + } + } else { + o.R.User = related + } + + if related.R == nil { + related.R = &userR{ + Notifications: NotificationSlice{o}, + } + } else { + related.R.Notifications = append(related.R.Notifications, o) + } + + return nil +} + +// Notifications retrieves all the records using an executor. +func Notifications(mods ...qm.QueryMod) notificationQuery { + mods = append(mods, qm.From("\"getstronger\".\"notifications\"")) + q := NewQuery(mods...) + if len(queries.GetSelect(q)) == 0 { + queries.SetSelect(q, []string{"\"getstronger\".\"notifications\".*"}) + } + + return notificationQuery{q} +} + +// FindNotification retrieves a single record by ID with an executor. +// If selectCols is empty Find will return all columns. +func FindNotification(ctx context.Context, exec boil.ContextExecutor, iD string, selectCols ...string) (*Notification, error) { + notificationObj := &Notification{} + + sel := "*" + if len(selectCols) > 0 { + sel = strings.Join(strmangle.IdentQuoteSlice(dialect.LQ, dialect.RQ, selectCols), ",") + } + query := fmt.Sprintf( + "select %s from \"getstronger\".\"notifications\" where \"id\"=$1", sel, + ) + + q := queries.Raw(query, iD) + + err := q.Bind(ctx, exec, notificationObj) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, sql.ErrNoRows + } + return nil, errors.Wrap(err, "orm: unable to select from notifications") + } + + if err = notificationObj.doAfterSelectHooks(ctx, exec); err != nil { + return notificationObj, err + } + + return notificationObj, nil +} + +// Insert a single record using an executor. +// See boil.Columns.InsertColumnSet documentation to understand column list inference for inserts. +func (o *Notification) Insert(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) error { + if o == nil { + return errors.New("orm: no notifications provided for insertion") + } + + var err error + if !boil.TimestampsAreSkipped(ctx) { + currTime := time.Now().In(boil.GetLocation()) + + if o.CreatedAt.IsZero() { + o.CreatedAt = currTime + } + } + + if err := o.doBeforeInsertHooks(ctx, exec); err != nil { + return err + } + + nzDefaults := queries.NonZeroDefaultSet(notificationColumnsWithDefault, o) + + key := makeCacheKey(columns, nzDefaults) + notificationInsertCacheMut.RLock() + cache, cached := notificationInsertCache[key] + notificationInsertCacheMut.RUnlock() + + if !cached { + wl, returnColumns := columns.InsertColumnSet( + notificationAllColumns, + notificationColumnsWithDefault, + notificationColumnsWithoutDefault, + nzDefaults, + ) + + cache.valueMapping, err = queries.BindMapping(notificationType, notificationMapping, wl) + if err != nil { + return err + } + cache.retMapping, err = queries.BindMapping(notificationType, notificationMapping, returnColumns) + if err != nil { + return err + } + if len(wl) != 0 { + cache.query = fmt.Sprintf("INSERT INTO \"getstronger\".\"notifications\" (\"%s\") %%sVALUES (%s)%%s", strings.Join(wl, "\",\""), strmangle.Placeholders(dialect.UseIndexPlaceholders, len(wl), 1, 1)) + } else { + cache.query = "INSERT INTO \"getstronger\".\"notifications\" %sDEFAULT VALUES%s" + } + + var queryOutput, queryReturning string + + if len(cache.retMapping) != 0 { + queryReturning = fmt.Sprintf(" RETURNING \"%s\"", strings.Join(returnColumns, "\",\"")) + } + + cache.query = fmt.Sprintf(cache.query, queryOutput, queryReturning) + } + + value := reflect.Indirect(reflect.ValueOf(o)) + vals := queries.ValuesFromMapping(value, cache.valueMapping) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, cache.query) + fmt.Fprintln(writer, vals) + } + + if len(cache.retMapping) != 0 { + err = exec.QueryRowContext(ctx, cache.query, vals...).Scan(queries.PtrsFromMapping(value, cache.retMapping)...) + } else { + _, err = exec.ExecContext(ctx, cache.query, vals...) + } + + if err != nil { + return errors.Wrap(err, "orm: unable to insert into notifications") + } + + if !cached { + notificationInsertCacheMut.Lock() + notificationInsertCache[key] = cache + notificationInsertCacheMut.Unlock() + } + + return o.doAfterInsertHooks(ctx, exec) +} + +// Update uses an executor to update the Notification. +// See boil.Columns.UpdateColumnSet documentation to understand column list inference for updates. +// Update does not automatically update the record in case of default values. Use .Reload() to refresh the records. +func (o *Notification) Update(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) (int64, error) { + var err error + if err = o.doBeforeUpdateHooks(ctx, exec); err != nil { + return 0, err + } + key := makeCacheKey(columns, nil) + notificationUpdateCacheMut.RLock() + cache, cached := notificationUpdateCache[key] + notificationUpdateCacheMut.RUnlock() + + if !cached { + wl := columns.UpdateColumnSet( + notificationAllColumns, + notificationPrimaryKeyColumns, + ) + + if !columns.IsWhitelist() { + wl = strmangle.SetComplement(wl, []string{"created_at"}) + } + if len(wl) == 0 { + return 0, errors.New("orm: unable to update notifications, could not build whitelist") + } + + cache.query = fmt.Sprintf("UPDATE \"getstronger\".\"notifications\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 1, wl), + strmangle.WhereClause("\"", "\"", len(wl)+1, notificationPrimaryKeyColumns), + ) + cache.valueMapping, err = queries.BindMapping(notificationType, notificationMapping, append(wl, notificationPrimaryKeyColumns...)) + if err != nil { + return 0, err + } + } + + values := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), cache.valueMapping) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, cache.query) + fmt.Fprintln(writer, values) + } + var result sql.Result + result, err = exec.ExecContext(ctx, cache.query, values...) + if err != nil { + return 0, errors.Wrap(err, "orm: unable to update notifications row") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "orm: failed to get rows affected by update for notifications") + } + + if !cached { + notificationUpdateCacheMut.Lock() + notificationUpdateCache[key] = cache + notificationUpdateCacheMut.Unlock() + } + + return rowsAff, o.doAfterUpdateHooks(ctx, exec) +} + +// UpdateAll updates all rows with the specified column values. +func (q notificationQuery) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) { + queries.SetUpdate(q.Query, cols) + + result, err := q.Query.ExecContext(ctx, exec) + if err != nil { + return 0, errors.Wrap(err, "orm: unable to update all for notifications") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "orm: unable to retrieve rows affected for notifications") + } + + return rowsAff, nil +} + +// UpdateAll updates all rows with the specified column values, using an executor. +func (o NotificationSlice) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) { + ln := int64(len(o)) + if ln == 0 { + return 0, nil + } + + if len(cols) == 0 { + return 0, errors.New("orm: update all requires at least one column argument") + } + + colNames := make([]string, len(cols)) + args := make([]interface{}, len(cols)) + + i := 0 + for name, value := range cols { + colNames[i] = name + args[i] = value + i++ + } + + // Append all of the primary key values for each column + for _, obj := range o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), notificationPrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := fmt.Sprintf("UPDATE \"getstronger\".\"notifications\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 1, colNames), + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), len(colNames)+1, notificationPrimaryKeyColumns, len(o))) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, args...) + } + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "orm: unable to update all in notification slice") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "orm: unable to retrieve rows affected all in update all notification") + } + return rowsAff, nil +} + +// Upsert attempts an insert using an executor, and does an update or ignore on conflict. +// See boil.Columns documentation for how to properly use updateColumns and insertColumns. +func (o *Notification) Upsert(ctx context.Context, exec boil.ContextExecutor, updateOnConflict bool, conflictColumns []string, updateColumns, insertColumns boil.Columns, opts ...UpsertOptionFunc) error { + if o == nil { + return errors.New("orm: no notifications provided for upsert") + } + if !boil.TimestampsAreSkipped(ctx) { + currTime := time.Now().In(boil.GetLocation()) + + if o.CreatedAt.IsZero() { + o.CreatedAt = currTime + } + } + + if err := o.doBeforeUpsertHooks(ctx, exec); err != nil { + return err + } + + nzDefaults := queries.NonZeroDefaultSet(notificationColumnsWithDefault, o) + + // Build cache key in-line uglily - mysql vs psql problems + buf := strmangle.GetBuffer() + if updateOnConflict { + buf.WriteByte('t') + } else { + buf.WriteByte('f') + } + buf.WriteByte('.') + for _, c := range conflictColumns { + buf.WriteString(c) + } + buf.WriteByte('.') + buf.WriteString(strconv.Itoa(updateColumns.Kind)) + for _, c := range updateColumns.Cols { + buf.WriteString(c) + } + buf.WriteByte('.') + buf.WriteString(strconv.Itoa(insertColumns.Kind)) + for _, c := range insertColumns.Cols { + buf.WriteString(c) + } + buf.WriteByte('.') + for _, c := range nzDefaults { + buf.WriteString(c) + } + key := buf.String() + strmangle.PutBuffer(buf) + + notificationUpsertCacheMut.RLock() + cache, cached := notificationUpsertCache[key] + notificationUpsertCacheMut.RUnlock() + + var err error + + if !cached { + insert, _ := insertColumns.InsertColumnSet( + notificationAllColumns, + notificationColumnsWithDefault, + notificationColumnsWithoutDefault, + nzDefaults, + ) + + update := updateColumns.UpdateColumnSet( + notificationAllColumns, + notificationPrimaryKeyColumns, + ) + + if updateOnConflict && len(update) == 0 { + return errors.New("orm: unable to upsert notifications, could not build update column list") + } + + ret := strmangle.SetComplement(notificationAllColumns, strmangle.SetIntersect(insert, update)) + + conflict := conflictColumns + if len(conflict) == 0 && updateOnConflict && len(update) != 0 { + if len(notificationPrimaryKeyColumns) == 0 { + return errors.New("orm: unable to upsert notifications, could not build conflict column list") + } + + conflict = make([]string, len(notificationPrimaryKeyColumns)) + copy(conflict, notificationPrimaryKeyColumns) + } + cache.query = buildUpsertQueryPostgres(dialect, "\"getstronger\".\"notifications\"", updateOnConflict, ret, update, conflict, insert, opts...) + + cache.valueMapping, err = queries.BindMapping(notificationType, notificationMapping, insert) + if err != nil { + return err + } + if len(ret) != 0 { + cache.retMapping, err = queries.BindMapping(notificationType, notificationMapping, ret) + if err != nil { + return err + } + } + } + + value := reflect.Indirect(reflect.ValueOf(o)) + vals := queries.ValuesFromMapping(value, cache.valueMapping) + var returns []interface{} + if len(cache.retMapping) != 0 { + returns = queries.PtrsFromMapping(value, cache.retMapping) + } + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, cache.query) + fmt.Fprintln(writer, vals) + } + if len(cache.retMapping) != 0 { + err = exec.QueryRowContext(ctx, cache.query, vals...).Scan(returns...) + if errors.Is(err, sql.ErrNoRows) { + err = nil // Postgres doesn't return anything when there's no update + } + } else { + _, err = exec.ExecContext(ctx, cache.query, vals...) + } + if err != nil { + return errors.Wrap(err, "orm: unable to upsert notifications") + } + + if !cached { + notificationUpsertCacheMut.Lock() + notificationUpsertCache[key] = cache + notificationUpsertCacheMut.Unlock() + } + + return o.doAfterUpsertHooks(ctx, exec) +} + +// Delete deletes a single Notification record with an executor. +// Delete will match against the primary key column to find the record to delete. +func (o *Notification) Delete(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if o == nil { + return 0, errors.New("orm: no Notification provided for delete") + } + + if err := o.doBeforeDeleteHooks(ctx, exec); err != nil { + return 0, err + } + + args := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), notificationPrimaryKeyMapping) + sql := "DELETE FROM \"getstronger\".\"notifications\" WHERE \"id\"=$1" + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, args...) + } + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "orm: unable to delete from notifications") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "orm: failed to get rows affected by delete for notifications") + } + + if err := o.doAfterDeleteHooks(ctx, exec); err != nil { + return 0, err + } + + return rowsAff, nil +} + +// DeleteAll deletes all matching rows. +func (q notificationQuery) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if q.Query == nil { + return 0, errors.New("orm: no notificationQuery provided for delete all") + } + + queries.SetDelete(q.Query) + + result, err := q.Query.ExecContext(ctx, exec) + if err != nil { + return 0, errors.Wrap(err, "orm: unable to delete all from notifications") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "orm: failed to get rows affected by deleteall for notifications") + } + + return rowsAff, nil +} + +// DeleteAll deletes all rows in the slice, using an executor. +func (o NotificationSlice) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if len(o) == 0 { + return 0, nil + } + + if len(notificationBeforeDeleteHooks) != 0 { + for _, obj := range o { + if err := obj.doBeforeDeleteHooks(ctx, exec); err != nil { + return 0, err + } + } + } + + var args []interface{} + for _, obj := range o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), notificationPrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := "DELETE FROM \"getstronger\".\"notifications\" WHERE " + + strmangle.WhereInClause(string(dialect.LQ), string(dialect.RQ), 1, notificationPrimaryKeyColumns, len(o)) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, args) + } + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "orm: unable to delete all from notification slice") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "orm: failed to get rows affected by deleteall for notifications") + } + + if len(notificationAfterDeleteHooks) != 0 { + for _, obj := range o { + if err := obj.doAfterDeleteHooks(ctx, exec); err != nil { + return 0, err + } + } + } + + return rowsAff, nil +} + +// Reload refetches the object from the database +// using the primary keys with an executor. +func (o *Notification) Reload(ctx context.Context, exec boil.ContextExecutor) error { + ret, err := FindNotification(ctx, exec, o.ID) + if err != nil { + return err + } + + *o = *ret + return nil +} + +// ReloadAll refetches every row with matching primary key column values +// and overwrites the original object slice with the newly updated slice. +func (o *NotificationSlice) ReloadAll(ctx context.Context, exec boil.ContextExecutor) error { + if o == nil || len(*o) == 0 { + return nil + } + + slice := NotificationSlice{} + var args []interface{} + for _, obj := range *o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), notificationPrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := "SELECT \"getstronger\".\"notifications\".* FROM \"getstronger\".\"notifications\" WHERE " + + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 1, notificationPrimaryKeyColumns, len(*o)) + + q := queries.Raw(sql, args...) + + err := q.Bind(ctx, exec, &slice) + if err != nil { + return errors.Wrap(err, "orm: unable to reload all in NotificationSlice") + } + + *o = slice + + return nil +} + +// NotificationExists checks if the Notification row exists. +func NotificationExists(ctx context.Context, exec boil.ContextExecutor, iD string) (bool, error) { + var exists bool + sql := "select exists(select 1 from \"getstronger\".\"notifications\" where \"id\"=$1 limit 1)" + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, iD) + } + row := exec.QueryRowContext(ctx, sql, iD) + + err := row.Scan(&exists) + if err != nil { + return false, errors.Wrap(err, "orm: unable to check if notifications exists") + } + + return exists, nil +} + +// Exists checks if the Notification row exists. +func (o *Notification) Exists(ctx context.Context, exec boil.ContextExecutor) (bool, error) { + return NotificationExists(ctx, exec, o.ID) +} diff --git a/server/pkg/orm/routines.go b/server/pkg/orm/routines.go index 55eb3009..055019b4 100644 --- a/server/pkg/orm/routines.go +++ b/server/pkg/orm/routines.go @@ -70,27 +70,6 @@ var RoutineTableColumns = struct { // Generated where -type whereHelpertypes_JSON struct{ field string } - -func (w whereHelpertypes_JSON) EQ(x types.JSON) qm.QueryMod { - return qmhelper.Where(w.field, qmhelper.EQ, x) -} -func (w whereHelpertypes_JSON) NEQ(x types.JSON) qm.QueryMod { - return qmhelper.Where(w.field, qmhelper.NEQ, x) -} -func (w whereHelpertypes_JSON) LT(x types.JSON) qm.QueryMod { - return qmhelper.Where(w.field, qmhelper.LT, x) -} -func (w whereHelpertypes_JSON) LTE(x types.JSON) qm.QueryMod { - return qmhelper.Where(w.field, qmhelper.LTE, x) -} -func (w whereHelpertypes_JSON) GT(x types.JSON) qm.QueryMod { - return qmhelper.Where(w.field, qmhelper.GT, x) -} -func (w whereHelpertypes_JSON) GTE(x types.JSON) qm.QueryMod { - return qmhelper.Where(w.field, qmhelper.GTE, x) -} - var RoutineWhere = struct { ID whereHelperstring UserID whereHelperstring diff --git a/server/pkg/orm/users.go b/server/pkg/orm/users.go index a35746d2..5320d000 100644 --- a/server/pkg/orm/users.go +++ b/server/pkg/orm/users.go @@ -83,6 +83,7 @@ var UserRels = struct { Exercises string FollowerUsers string FolloweeUsers string + Notifications string Routines string WorkoutComments string Workouts string @@ -91,6 +92,7 @@ var UserRels = struct { Exercises: "Exercises", FollowerUsers: "FollowerUsers", FolloweeUsers: "FolloweeUsers", + Notifications: "Notifications", Routines: "Routines", WorkoutComments: "WorkoutComments", Workouts: "Workouts", @@ -102,6 +104,7 @@ type userR struct { Exercises ExerciseSlice `boil:"Exercises" json:"Exercises" toml:"Exercises" yaml:"Exercises"` FollowerUsers UserSlice `boil:"FollowerUsers" json:"FollowerUsers" toml:"FollowerUsers" yaml:"FollowerUsers"` FolloweeUsers UserSlice `boil:"FolloweeUsers" json:"FolloweeUsers" toml:"FolloweeUsers" yaml:"FolloweeUsers"` + Notifications NotificationSlice `boil:"Notifications" json:"Notifications" toml:"Notifications" yaml:"Notifications"` Routines RoutineSlice `boil:"Routines" json:"Routines" toml:"Routines" yaml:"Routines"` WorkoutComments WorkoutCommentSlice `boil:"WorkoutComments" json:"WorkoutComments" toml:"WorkoutComments" yaml:"WorkoutComments"` Workouts WorkoutSlice `boil:"Workouts" json:"Workouts" toml:"Workouts" yaml:"Workouts"` @@ -140,6 +143,13 @@ func (r *userR) GetFolloweeUsers() UserSlice { return r.FolloweeUsers } +func (r *userR) GetNotifications() NotificationSlice { + if r == nil { + return nil + } + return r.Notifications +} + func (r *userR) GetRoutines() RoutineSlice { if r == nil { return nil @@ -532,6 +542,20 @@ func (o *User) FolloweeUsers(mods ...qm.QueryMod) userQuery { return Users(queryMods...) } +// Notifications retrieves all the notification's Notifications with an executor. +func (o *User) Notifications(mods ...qm.QueryMod) notificationQuery { + var queryMods []qm.QueryMod + if len(mods) != 0 { + queryMods = append(queryMods, mods...) + } + + queryMods = append(queryMods, + qm.Where("\"getstronger\".\"notifications\".\"user_id\"=?", o.ID), + ) + + return Notifications(queryMods...) +} + // Routines retrieves all the routine's Routines with an executor. func (o *User) Routines(mods ...qm.QueryMod) routineQuery { var queryMods []qm.QueryMod @@ -1067,6 +1091,119 @@ func (userL) LoadFolloweeUsers(ctx context.Context, e boil.ContextExecutor, sing return nil } +// LoadNotifications allows an eager lookup of values, cached into the +// loaded structs of the objects. This is for a 1-M or N-M relationship. +func (userL) LoadNotifications(ctx context.Context, e boil.ContextExecutor, singular bool, maybeUser interface{}, mods queries.Applicator) error { + var slice []*User + var object *User + + if singular { + var ok bool + object, ok = maybeUser.(*User) + if !ok { + object = new(User) + ok = queries.SetFromEmbeddedStruct(&object, &maybeUser) + if !ok { + return errors.New(fmt.Sprintf("failed to set %T from embedded struct %T", object, maybeUser)) + } + } + } else { + s, ok := maybeUser.(*[]*User) + if ok { + slice = *s + } else { + ok = queries.SetFromEmbeddedStruct(&slice, maybeUser) + if !ok { + return errors.New(fmt.Sprintf("failed to set %T from embedded struct %T", slice, maybeUser)) + } + } + } + + args := make(map[interface{}]struct{}) + if singular { + if object.R == nil { + object.R = &userR{} + } + args[object.ID] = struct{}{} + } else { + for _, obj := range slice { + if obj.R == nil { + obj.R = &userR{} + } + args[obj.ID] = struct{}{} + } + } + + if len(args) == 0 { + return nil + } + + argsSlice := make([]interface{}, len(args)) + i := 0 + for arg := range args { + argsSlice[i] = arg + i++ + } + + query := NewQuery( + qm.From(`getstronger.notifications`), + qm.WhereIn(`getstronger.notifications.user_id in ?`, argsSlice...), + ) + if mods != nil { + mods.Apply(query) + } + + results, err := query.QueryContext(ctx, e) + if err != nil { + return errors.Wrap(err, "failed to eager load notifications") + } + + var resultSlice []*Notification + if err = queries.Bind(results, &resultSlice); err != nil { + return errors.Wrap(err, "failed to bind eager loaded slice notifications") + } + + if err = results.Close(); err != nil { + return errors.Wrap(err, "failed to close results in eager load on notifications") + } + if err = results.Err(); err != nil { + return errors.Wrap(err, "error occurred during iteration of eager loaded relations for notifications") + } + + if len(notificationAfterSelectHooks) != 0 { + for _, obj := range resultSlice { + if err := obj.doAfterSelectHooks(ctx, e); err != nil { + return err + } + } + } + if singular { + object.R.Notifications = resultSlice + for _, foreign := range resultSlice { + if foreign.R == nil { + foreign.R = ¬ificationR{} + } + foreign.R.User = object + } + return nil + } + + for _, foreign := range resultSlice { + for _, local := range slice { + if local.ID == foreign.UserID { + local.R.Notifications = append(local.R.Notifications, foreign) + if foreign.R == nil { + foreign.R = ¬ificationR{} + } + foreign.R.User = local + break + } + } + } + + return nil +} + // LoadRoutines allows an eager lookup of values, cached into the // loaded structs of the objects. This is for a 1-M or N-M relationship. func (userL) LoadRoutines(ctx context.Context, e boil.ContextExecutor, singular bool, maybeUser interface{}, mods queries.Applicator) error { @@ -1796,6 +1933,59 @@ func removeFolloweeUsersFromFollowerUsersSlice(o *User, related []*User) { } } +// AddNotifications adds the given related objects to the existing relationships +// of the user, optionally inserting them as new records. +// Appends related to o.R.Notifications. +// Sets related.R.User appropriately. +func (o *User) AddNotifications(ctx context.Context, exec boil.ContextExecutor, insert bool, related ...*Notification) error { + var err error + for _, rel := range related { + if insert { + rel.UserID = o.ID + if err = rel.Insert(ctx, exec, boil.Infer()); err != nil { + return errors.Wrap(err, "failed to insert into foreign table") + } + } else { + updateQuery := fmt.Sprintf( + "UPDATE \"getstronger\".\"notifications\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 1, []string{"user_id"}), + strmangle.WhereClause("\"", "\"", 2, notificationPrimaryKeyColumns), + ) + values := []interface{}{o.ID, rel.ID} + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, updateQuery) + fmt.Fprintln(writer, values) + } + if _, err = exec.ExecContext(ctx, updateQuery, values...); err != nil { + return errors.Wrap(err, "failed to update foreign table") + } + + rel.UserID = o.ID + } + } + + if o.R == nil { + o.R = &userR{ + Notifications: related, + } + } else { + o.R.Notifications = append(o.R.Notifications, related...) + } + + for _, rel := range related { + if rel.R == nil { + rel.R = ¬ificationR{ + User: o, + } + } else { + rel.R.User = o + } + } + return nil +} + // AddRoutines adds the given related objects to the existing relationships // of the user, optionally inserting them as new records. // Appends related to o.R.Routines. diff --git a/server/pkg/pb/api/v1/apiv1connect/users.connect.go b/server/pkg/pb/api/v1/apiv1connect/users.connect.go index 4ed5a642..114071de 100644 --- a/server/pkg/pb/api/v1/apiv1connect/users.connect.go +++ b/server/pkg/pb/api/v1/apiv1connect/users.connect.go @@ -48,17 +48,21 @@ const ( UserServiceListFolloweesProcedure = "/api.v1.UserService/ListFollowees" // UserServiceSearchProcedure is the fully-qualified name of the UserService's Search RPC. UserServiceSearchProcedure = "/api.v1.UserService/Search" + // UserServiceListNotificationsProcedure is the fully-qualified name of the UserService's + // ListNotifications RPC. + UserServiceListNotificationsProcedure = "/api.v1.UserService/ListNotifications" ) // These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. var ( - userServiceServiceDescriptor = v1.File_api_v1_users_proto.Services().ByName("UserService") - userServiceGetMethodDescriptor = userServiceServiceDescriptor.Methods().ByName("Get") - userServiceFollowMethodDescriptor = userServiceServiceDescriptor.Methods().ByName("Follow") - userServiceUnfollowMethodDescriptor = userServiceServiceDescriptor.Methods().ByName("Unfollow") - userServiceListFollowersMethodDescriptor = userServiceServiceDescriptor.Methods().ByName("ListFollowers") - userServiceListFolloweesMethodDescriptor = userServiceServiceDescriptor.Methods().ByName("ListFollowees") - userServiceSearchMethodDescriptor = userServiceServiceDescriptor.Methods().ByName("Search") + userServiceServiceDescriptor = v1.File_api_v1_users_proto.Services().ByName("UserService") + userServiceGetMethodDescriptor = userServiceServiceDescriptor.Methods().ByName("Get") + userServiceFollowMethodDescriptor = userServiceServiceDescriptor.Methods().ByName("Follow") + userServiceUnfollowMethodDescriptor = userServiceServiceDescriptor.Methods().ByName("Unfollow") + userServiceListFollowersMethodDescriptor = userServiceServiceDescriptor.Methods().ByName("ListFollowers") + userServiceListFolloweesMethodDescriptor = userServiceServiceDescriptor.Methods().ByName("ListFollowees") + userServiceSearchMethodDescriptor = userServiceServiceDescriptor.Methods().ByName("Search") + userServiceListNotificationsMethodDescriptor = userServiceServiceDescriptor.Methods().ByName("ListNotifications") ) // UserServiceClient is a client for the api.v1.UserService service. @@ -69,6 +73,7 @@ type UserServiceClient interface { ListFollowers(context.Context, *connect.Request[v1.ListFollowersRequest]) (*connect.Response[v1.ListFollowersResponse], error) ListFollowees(context.Context, *connect.Request[v1.ListFolloweesRequest]) (*connect.Response[v1.ListFolloweesResponse], error) Search(context.Context, *connect.Request[v1.SearchRequest]) (*connect.Response[v1.SearchResponse], error) + ListNotifications(context.Context, *connect.Request[v1.ListNotificationsRequest]) (*connect.Response[v1.ListNotificationsResponse], error) } // NewUserServiceClient constructs a client for the api.v1.UserService service. By default, it uses @@ -117,17 +122,24 @@ func NewUserServiceClient(httpClient connect.HTTPClient, baseURL string, opts .. connect.WithSchema(userServiceSearchMethodDescriptor), connect.WithClientOptions(opts...), ), + listNotifications: connect.NewClient[v1.ListNotificationsRequest, v1.ListNotificationsResponse]( + httpClient, + baseURL+UserServiceListNotificationsProcedure, + connect.WithSchema(userServiceListNotificationsMethodDescriptor), + connect.WithClientOptions(opts...), + ), } } // userServiceClient implements UserServiceClient. type userServiceClient struct { - get *connect.Client[v1.GetUserRequest, v1.GetUserResponse] - follow *connect.Client[v1.FollowRequest, v1.FollowResponse] - unfollow *connect.Client[v1.UnfollowRequest, v1.UnfollowResponse] - listFollowers *connect.Client[v1.ListFollowersRequest, v1.ListFollowersResponse] - listFollowees *connect.Client[v1.ListFolloweesRequest, v1.ListFolloweesResponse] - search *connect.Client[v1.SearchRequest, v1.SearchResponse] + get *connect.Client[v1.GetUserRequest, v1.GetUserResponse] + follow *connect.Client[v1.FollowRequest, v1.FollowResponse] + unfollow *connect.Client[v1.UnfollowRequest, v1.UnfollowResponse] + listFollowers *connect.Client[v1.ListFollowersRequest, v1.ListFollowersResponse] + listFollowees *connect.Client[v1.ListFolloweesRequest, v1.ListFolloweesResponse] + search *connect.Client[v1.SearchRequest, v1.SearchResponse] + listNotifications *connect.Client[v1.ListNotificationsRequest, v1.ListNotificationsResponse] } // Get calls api.v1.UserService.Get. @@ -160,6 +172,11 @@ func (c *userServiceClient) Search(ctx context.Context, req *connect.Request[v1. return c.search.CallUnary(ctx, req) } +// ListNotifications calls api.v1.UserService.ListNotifications. +func (c *userServiceClient) ListNotifications(ctx context.Context, req *connect.Request[v1.ListNotificationsRequest]) (*connect.Response[v1.ListNotificationsResponse], error) { + return c.listNotifications.CallUnary(ctx, req) +} + // UserServiceHandler is an implementation of the api.v1.UserService service. type UserServiceHandler interface { Get(context.Context, *connect.Request[v1.GetUserRequest]) (*connect.Response[v1.GetUserResponse], error) @@ -168,6 +185,7 @@ type UserServiceHandler interface { ListFollowers(context.Context, *connect.Request[v1.ListFollowersRequest]) (*connect.Response[v1.ListFollowersResponse], error) ListFollowees(context.Context, *connect.Request[v1.ListFolloweesRequest]) (*connect.Response[v1.ListFolloweesResponse], error) Search(context.Context, *connect.Request[v1.SearchRequest]) (*connect.Response[v1.SearchResponse], error) + ListNotifications(context.Context, *connect.Request[v1.ListNotificationsRequest]) (*connect.Response[v1.ListNotificationsResponse], error) } // NewUserServiceHandler builds an HTTP handler from the service implementation. It returns the path @@ -212,6 +230,12 @@ func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption connect.WithSchema(userServiceSearchMethodDescriptor), connect.WithHandlerOptions(opts...), ) + userServiceListNotificationsHandler := connect.NewUnaryHandler( + UserServiceListNotificationsProcedure, + svc.ListNotifications, + connect.WithSchema(userServiceListNotificationsMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) return "/api.v1.UserService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case UserServiceGetProcedure: @@ -226,6 +250,8 @@ func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption userServiceListFolloweesHandler.ServeHTTP(w, r) case UserServiceSearchProcedure: userServiceSearchHandler.ServeHTTP(w, r) + case UserServiceListNotificationsProcedure: + userServiceListNotificationsHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -258,3 +284,7 @@ func (UnimplementedUserServiceHandler) ListFollowees(context.Context, *connect.R func (UnimplementedUserServiceHandler) Search(context.Context, *connect.Request[v1.SearchRequest]) (*connect.Response[v1.SearchResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("api.v1.UserService.Search is not implemented")) } + +func (UnimplementedUserServiceHandler) ListNotifications(context.Context, *connect.Request[v1.ListNotificationsRequest]) (*connect.Response[v1.ListNotificationsResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("api.v1.UserService.ListNotifications is not implemented")) +} diff --git a/server/pkg/pb/api/v1/users.pb.go b/server/pkg/pb/api/v1/users.pb.go index 290533dc..babe3463 100644 --- a/server/pkg/pb/api/v1/users.pb.go +++ b/server/pkg/pb/api/v1/users.pb.go @@ -13,6 +13,7 @@ import ( _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + _ "google.golang.org/protobuf/types/known/timestamppb" ) const ( @@ -560,6 +561,247 @@ func (x *SearchResponse) GetPagination() *PaginationResponse { return nil } +type ListNotificationsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + OnlyUnread bool `protobuf:"varint,1,opt,name=only_unread,json=onlyUnread,proto3" json:"only_unread,omitempty"` + Pagination *PaginationRequest `protobuf:"bytes,2,opt,name=pagination,proto3" json:"pagination,omitempty"` +} + +func (x *ListNotificationsRequest) Reset() { + *x = ListNotificationsRequest{} + mi := &file_api_v1_users_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListNotificationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListNotificationsRequest) ProtoMessage() {} + +func (x *ListNotificationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_users_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListNotificationsRequest.ProtoReflect.Descriptor instead. +func (*ListNotificationsRequest) Descriptor() ([]byte, []int) { + return file_api_v1_users_proto_rawDescGZIP(), []int{12} +} + +func (x *ListNotificationsRequest) GetOnlyUnread() bool { + if x != nil { + return x.OnlyUnread + } + return false +} + +func (x *ListNotificationsRequest) GetPagination() *PaginationRequest { + if x != nil { + return x.Pagination + } + return nil +} + +type ListNotificationsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Notifications []*Notification `protobuf:"bytes,1,rep,name=notifications,proto3" json:"notifications,omitempty"` + Pagination *PaginationResponse `protobuf:"bytes,2,opt,name=pagination,proto3" json:"pagination,omitempty"` +} + +func (x *ListNotificationsResponse) Reset() { + *x = ListNotificationsResponse{} + mi := &file_api_v1_users_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListNotificationsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListNotificationsResponse) ProtoMessage() {} + +func (x *ListNotificationsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_users_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListNotificationsResponse.ProtoReflect.Descriptor instead. +func (*ListNotificationsResponse) Descriptor() ([]byte, []int) { + return file_api_v1_users_proto_rawDescGZIP(), []int{13} +} + +func (x *ListNotificationsResponse) GetNotifications() []*Notification { + if x != nil { + return x.Notifications + } + return nil +} + +func (x *ListNotificationsResponse) GetPagination() *PaginationResponse { + if x != nil { + return x.Pagination + } + return nil +} + +type Notification struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // DEBT: This should be a timestamp but the client is not able to parse it. + NotifiedAtUnix int64 `protobuf:"varint,2,opt,name=notified_at_unix,json=notifiedAtUnix,proto3" json:"notified_at_unix,omitempty"` + // Types that are assignable to Type: + // + // *Notification_WorkoutComment_ + Type isNotification_Type `protobuf_oneof:"type"` +} + +func (x *Notification) Reset() { + *x = Notification{} + mi := &file_api_v1_users_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Notification) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Notification) ProtoMessage() {} + +func (x *Notification) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_users_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Notification.ProtoReflect.Descriptor instead. +func (*Notification) Descriptor() ([]byte, []int) { + return file_api_v1_users_proto_rawDescGZIP(), []int{14} +} + +func (x *Notification) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Notification) GetNotifiedAtUnix() int64 { + if x != nil { + return x.NotifiedAtUnix + } + return 0 +} + +func (m *Notification) GetType() isNotification_Type { + if m != nil { + return m.Type + } + return nil +} + +func (x *Notification) GetWorkoutComment() *Notification_WorkoutComment { + if x, ok := x.GetType().(*Notification_WorkoutComment_); ok { + return x.WorkoutComment + } + return nil +} + +type isNotification_Type interface { + isNotification_Type() +} + +type Notification_WorkoutComment_ struct { + WorkoutComment *Notification_WorkoutComment `protobuf:"bytes,3,opt,name=workout_comment,json=workoutComment,proto3,oneof"` +} + +func (*Notification_WorkoutComment_) isNotification_Type() {} + +type Notification_WorkoutComment struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Actor *User `protobuf:"bytes,1,opt,name=actor,proto3" json:"actor,omitempty"` + Workout *Workout `protobuf:"bytes,2,opt,name=workout,proto3" json:"workout,omitempty"` +} + +func (x *Notification_WorkoutComment) Reset() { + *x = Notification_WorkoutComment{} + mi := &file_api_v1_users_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Notification_WorkoutComment) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Notification_WorkoutComment) ProtoMessage() {} + +func (x *Notification_WorkoutComment) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_users_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Notification_WorkoutComment.ProtoReflect.Descriptor instead. +func (*Notification_WorkoutComment) Descriptor() ([]byte, []int) { + return file_api_v1_users_proto_rawDescGZIP(), []int{14, 0} +} + +func (x *Notification_WorkoutComment) GetActor() *User { + if x != nil { + return x.Actor + } + return nil +} + +func (x *Notification_WorkoutComment) GetWorkout() *Workout { + if x != nil { + return x.Workout + } + return nil +} + var File_api_v1_users_proto protoreflect.FileDescriptor var file_api_v1_users_proto_rawDesc = []byte{ @@ -567,93 +809,136 @@ var file_api_v1_users_proto_rawDesc = []byte{ 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x1a, 0x14, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x13, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x68, 0x61, 0x72, 0x65, - 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x62, 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, - 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2a, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, - 0x22, 0x33, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, - 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x36, 0x0a, 0x0d, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x09, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, - 0xb0, 0x01, 0x01, 0x52, 0x08, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x22, 0x10, 0x0a, - 0x0e, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x3c, 0x0a, 0x0f, 0x55, 0x6e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x29, 0x0a, 0x0b, 0x75, 0x6e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, - 0x01, 0x52, 0x0a, 0x75, 0x6e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x22, 0x12, 0x0a, - 0x10, 0x55, 0x6e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x41, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, - 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x0b, 0x66, 0x6f, 0x6c, - 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, - 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0a, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, - 0x65, 0x72, 0x49, 0x64, 0x22, 0x43, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x6f, 0x6c, 0x6c, - 0x6f, 0x77, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, - 0x09, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x09, - 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x22, 0x41, 0x0a, 0x14, 0x4c, 0x69, 0x73, - 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x29, 0x0a, 0x0b, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x5f, 0x69, 0x64, + 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x15, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, + 0x77, 0x6f, 0x72, 0x6b, 0x6f, 0x75, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, + 0x1b, 0x62, 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2a, 0x0a, 0x0e, + 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, + 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, + 0x03, 0xb0, 0x01, 0x01, 0x52, 0x02, 0x69, 0x64, 0x22, 0x33, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x55, + 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x04, 0x75, + 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x36, 0x0a, + 0x0d, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, + 0x0a, 0x09, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x08, 0x66, 0x6f, 0x6c, + 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x22, 0x10, 0x0a, 0x0e, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3c, 0x0a, 0x0f, 0x55, 0x6e, 0x66, 0x6f, 0x6c, + 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x0b, 0x75, 0x6e, + 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0a, 0x75, 0x6e, 0x66, 0x6f, 0x6c, + 0x6c, 0x6f, 0x77, 0x49, 0x64, 0x22, 0x12, 0x0a, 0x10, 0x55, 0x6e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, + 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x41, 0x0a, 0x14, 0x4c, 0x69, 0x73, + 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x29, 0x0a, 0x0b, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, - 0x52, 0x0a, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x49, 0x64, 0x22, 0x43, 0x0a, 0x15, - 0x4c, 0x69, 0x73, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x73, 0x52, 0x65, 0x73, + 0x52, 0x0a, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x49, 0x64, 0x22, 0x43, 0x0a, 0x15, + 0x4c, 0x69, 0x73, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x09, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, - 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x09, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, - 0x73, 0x22, 0x71, 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x03, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, - 0x79, 0x12, 0x41, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x50, - 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x70, 0x0a, 0x0e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x55, - 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, - 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, - 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0xb6, 0x03, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3c, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x16, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x04, - 0x88, 0xb5, 0x18, 0x01, 0x12, 0x3d, 0x0a, 0x06, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x12, 0x15, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x46, - 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x04, 0x88, - 0xb5, 0x18, 0x01, 0x12, 0x43, 0x0a, 0x08, 0x55, 0x6e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x12, - 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, - 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, - 0x31, 0x2e, 0x55, 0x6e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x04, 0x88, 0xb5, 0x18, 0x01, 0x12, 0x52, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, - 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, + 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x09, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, + 0x73, 0x22, 0x41, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, + 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x0b, 0x66, 0x6f, 0x6c, + 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, + 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x0a, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, + 0x65, 0x65, 0x49, 0x64, 0x22, 0x43, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x6f, 0x6c, 0x6c, + 0x6f, 0x77, 0x65, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, + 0x09, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x09, + 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x73, 0x22, 0x71, 0x0a, 0x0d, 0x53, 0x65, 0x61, + 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x05, 0x71, 0x75, + 0x65, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, + 0x10, 0x03, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x41, 0x0a, 0x0a, 0x70, 0x61, 0x67, + 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, + 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x70, 0x0a, 0x0e, + 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x22, + 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, + 0x72, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, + 0x50, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x7e, + 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6f, 0x6e, + 0x6c, 0x79, 0x5f, 0x75, 0x6e, 0x72, 0x65, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0a, 0x6f, 0x6e, 0x6c, 0x79, 0x55, 0x6e, 0x72, 0x65, 0x61, 0x64, 0x12, 0x41, 0x0a, 0x0a, 0x70, + 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, + 0x01, 0x01, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x93, + 0x01, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0d, + 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x69, + 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x67, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x69, 0x6e, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x81, 0x02, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x65, + 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x75, 0x6e, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x74, 0x55, 0x6e, 0x69, 0x78, 0x12, + 0x4e, 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x6f, 0x75, 0x74, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x65, + 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, + 0x31, 0x2e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x57, + 0x6f, 0x72, 0x6b, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, + 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x1a, + 0x5f, 0x0a, 0x0e, 0x57, 0x6f, 0x72, 0x6b, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, + 0x74, 0x12, 0x22, 0x0a, 0x05, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x0c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, + 0x61, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x29, 0x0a, 0x07, 0x77, 0x6f, 0x72, 0x6b, 0x6f, 0x75, 0x74, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, + 0x57, 0x6f, 0x72, 0x6b, 0x6f, 0x75, 0x74, 0x52, 0x07, 0x77, 0x6f, 0x72, 0x6b, 0x6f, 0x75, 0x74, + 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x32, 0x96, 0x04, 0x0a, 0x0b, 0x55, 0x73, 0x65, + 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3c, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, + 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, + 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x04, 0x88, 0xb5, 0x18, 0x01, 0x12, 0x3d, 0x0a, 0x06, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, + 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, + 0x2e, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x04, 0x88, 0xb5, 0x18, 0x01, 0x12, 0x43, 0x0a, 0x08, 0x55, 0x6e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, + 0x77, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x66, 0x6f, 0x6c, + 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x04, 0x88, 0xb5, 0x18, 0x01, 0x12, 0x52, 0x0a, 0x0d, 0x4c, 0x69, + 0x73, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, + 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x04, 0x88, 0xb5, 0x18, 0x01, 0x12, 0x52, 0x0a, 0x0d, - 0x4c, 0x69, 0x73, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x73, 0x12, 0x1c, 0x2e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x04, 0x88, 0xb5, 0x18, 0x01, 0x12, 0x52, + 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x73, 0x12, + 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x6f, 0x6c, + 0x6c, 0x6f, 0x77, 0x65, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, - 0x77, 0x65, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x65, - 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x04, 0x88, 0xb5, 0x18, 0x01, - 0x12, 0x3d, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, - 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x04, 0x88, 0xb5, 0x18, 0x01, 0x42, - 0x8b, 0x01, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x42, 0x0a, - 0x55, 0x73, 0x65, 0x72, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x38, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x72, 0x6c, 0x73, 0x73, 0x6e, 0x2f, - 0x67, 0x65, 0x74, 0x73, 0x74, 0x72, 0x6f, 0x6e, 0x67, 0x65, 0x72, 0x2f, 0x73, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x62, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, - 0x3b, 0x61, 0x70, 0x69, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02, 0x06, 0x41, - 0x70, 0x69, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x06, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x31, 0xe2, 0x02, - 0x12, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0xea, 0x02, 0x07, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x77, 0x65, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x04, 0x88, 0xb5, + 0x18, 0x01, 0x12, 0x3d, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x15, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x61, + 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x04, 0x88, 0xb5, 0x18, + 0x01, 0x12, 0x5e, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, + 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x04, 0x88, 0xb5, 0x18, + 0x01, 0x42, 0x8b, 0x01, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, + 0x42, 0x0a, 0x55, 0x73, 0x65, 0x72, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x38, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x72, 0x6c, 0x73, 0x73, + 0x6e, 0x2f, 0x67, 0x65, 0x74, 0x73, 0x74, 0x72, 0x6f, 0x6e, 0x67, 0x65, 0x72, 0x2f, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x62, 0x2f, 0x61, 0x70, 0x69, 0x2f, + 0x76, 0x31, 0x3b, 0x61, 0x70, 0x69, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02, + 0x06, 0x41, 0x70, 0x69, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x06, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x31, + 0xe2, 0x02, 0x12, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x07, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x56, 0x31, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -668,48 +953,61 @@ func file_api_v1_users_proto_rawDescGZIP() []byte { return file_api_v1_users_proto_rawDescData } -var file_api_v1_users_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_api_v1_users_proto_msgTypes = make([]protoimpl.MessageInfo, 16) var file_api_v1_users_proto_goTypes = []any{ - (*GetUserRequest)(nil), // 0: api.v1.GetUserRequest - (*GetUserResponse)(nil), // 1: api.v1.GetUserResponse - (*FollowRequest)(nil), // 2: api.v1.FollowRequest - (*FollowResponse)(nil), // 3: api.v1.FollowResponse - (*UnfollowRequest)(nil), // 4: api.v1.UnfollowRequest - (*UnfollowResponse)(nil), // 5: api.v1.UnfollowResponse - (*ListFollowersRequest)(nil), // 6: api.v1.ListFollowersRequest - (*ListFollowersResponse)(nil), // 7: api.v1.ListFollowersResponse - (*ListFolloweesRequest)(nil), // 8: api.v1.ListFolloweesRequest - (*ListFolloweesResponse)(nil), // 9: api.v1.ListFolloweesResponse - (*SearchRequest)(nil), // 10: api.v1.SearchRequest - (*SearchResponse)(nil), // 11: api.v1.SearchResponse - (*User)(nil), // 12: api.v1.User - (*PaginationRequest)(nil), // 13: api.v1.PaginationRequest - (*PaginationResponse)(nil), // 14: api.v1.PaginationResponse + (*GetUserRequest)(nil), // 0: api.v1.GetUserRequest + (*GetUserResponse)(nil), // 1: api.v1.GetUserResponse + (*FollowRequest)(nil), // 2: api.v1.FollowRequest + (*FollowResponse)(nil), // 3: api.v1.FollowResponse + (*UnfollowRequest)(nil), // 4: api.v1.UnfollowRequest + (*UnfollowResponse)(nil), // 5: api.v1.UnfollowResponse + (*ListFollowersRequest)(nil), // 6: api.v1.ListFollowersRequest + (*ListFollowersResponse)(nil), // 7: api.v1.ListFollowersResponse + (*ListFolloweesRequest)(nil), // 8: api.v1.ListFolloweesRequest + (*ListFolloweesResponse)(nil), // 9: api.v1.ListFolloweesResponse + (*SearchRequest)(nil), // 10: api.v1.SearchRequest + (*SearchResponse)(nil), // 11: api.v1.SearchResponse + (*ListNotificationsRequest)(nil), // 12: api.v1.ListNotificationsRequest + (*ListNotificationsResponse)(nil), // 13: api.v1.ListNotificationsResponse + (*Notification)(nil), // 14: api.v1.Notification + (*Notification_WorkoutComment)(nil), // 15: api.v1.Notification.WorkoutComment + (*User)(nil), // 16: api.v1.User + (*PaginationRequest)(nil), // 17: api.v1.PaginationRequest + (*PaginationResponse)(nil), // 18: api.v1.PaginationResponse + (*Workout)(nil), // 19: api.v1.Workout } var file_api_v1_users_proto_depIdxs = []int32{ - 12, // 0: api.v1.GetUserResponse.user:type_name -> api.v1.User - 12, // 1: api.v1.ListFollowersResponse.followers:type_name -> api.v1.User - 12, // 2: api.v1.ListFolloweesResponse.followees:type_name -> api.v1.User - 13, // 3: api.v1.SearchRequest.pagination:type_name -> api.v1.PaginationRequest - 12, // 4: api.v1.SearchResponse.users:type_name -> api.v1.User - 14, // 5: api.v1.SearchResponse.pagination:type_name -> api.v1.PaginationResponse - 0, // 6: api.v1.UserService.Get:input_type -> api.v1.GetUserRequest - 2, // 7: api.v1.UserService.Follow:input_type -> api.v1.FollowRequest - 4, // 8: api.v1.UserService.Unfollow:input_type -> api.v1.UnfollowRequest - 6, // 9: api.v1.UserService.ListFollowers:input_type -> api.v1.ListFollowersRequest - 8, // 10: api.v1.UserService.ListFollowees:input_type -> api.v1.ListFolloweesRequest - 10, // 11: api.v1.UserService.Search:input_type -> api.v1.SearchRequest - 1, // 12: api.v1.UserService.Get:output_type -> api.v1.GetUserResponse - 3, // 13: api.v1.UserService.Follow:output_type -> api.v1.FollowResponse - 5, // 14: api.v1.UserService.Unfollow:output_type -> api.v1.UnfollowResponse - 7, // 15: api.v1.UserService.ListFollowers:output_type -> api.v1.ListFollowersResponse - 9, // 16: api.v1.UserService.ListFollowees:output_type -> api.v1.ListFolloweesResponse - 11, // 17: api.v1.UserService.Search:output_type -> api.v1.SearchResponse - 12, // [12:18] is the sub-list for method output_type - 6, // [6:12] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 16, // 0: api.v1.GetUserResponse.user:type_name -> api.v1.User + 16, // 1: api.v1.ListFollowersResponse.followers:type_name -> api.v1.User + 16, // 2: api.v1.ListFolloweesResponse.followees:type_name -> api.v1.User + 17, // 3: api.v1.SearchRequest.pagination:type_name -> api.v1.PaginationRequest + 16, // 4: api.v1.SearchResponse.users:type_name -> api.v1.User + 18, // 5: api.v1.SearchResponse.pagination:type_name -> api.v1.PaginationResponse + 17, // 6: api.v1.ListNotificationsRequest.pagination:type_name -> api.v1.PaginationRequest + 14, // 7: api.v1.ListNotificationsResponse.notifications:type_name -> api.v1.Notification + 18, // 8: api.v1.ListNotificationsResponse.pagination:type_name -> api.v1.PaginationResponse + 15, // 9: api.v1.Notification.workout_comment:type_name -> api.v1.Notification.WorkoutComment + 16, // 10: api.v1.Notification.WorkoutComment.actor:type_name -> api.v1.User + 19, // 11: api.v1.Notification.WorkoutComment.workout:type_name -> api.v1.Workout + 0, // 12: api.v1.UserService.Get:input_type -> api.v1.GetUserRequest + 2, // 13: api.v1.UserService.Follow:input_type -> api.v1.FollowRequest + 4, // 14: api.v1.UserService.Unfollow:input_type -> api.v1.UnfollowRequest + 6, // 15: api.v1.UserService.ListFollowers:input_type -> api.v1.ListFollowersRequest + 8, // 16: api.v1.UserService.ListFollowees:input_type -> api.v1.ListFolloweesRequest + 10, // 17: api.v1.UserService.Search:input_type -> api.v1.SearchRequest + 12, // 18: api.v1.UserService.ListNotifications:input_type -> api.v1.ListNotificationsRequest + 1, // 19: api.v1.UserService.Get:output_type -> api.v1.GetUserResponse + 3, // 20: api.v1.UserService.Follow:output_type -> api.v1.FollowResponse + 5, // 21: api.v1.UserService.Unfollow:output_type -> api.v1.UnfollowResponse + 7, // 22: api.v1.UserService.ListFollowers:output_type -> api.v1.ListFollowersResponse + 9, // 23: api.v1.UserService.ListFollowees:output_type -> api.v1.ListFolloweesResponse + 11, // 24: api.v1.UserService.Search:output_type -> api.v1.SearchResponse + 13, // 25: api.v1.UserService.ListNotifications:output_type -> api.v1.ListNotificationsResponse + 19, // [19:26] is the sub-list for method output_type + 12, // [12:19] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name } func init() { file_api_v1_users_proto_init() } @@ -719,13 +1017,17 @@ func file_api_v1_users_proto_init() { } file_api_v1_options_proto_init() file_api_v1_shared_proto_init() + file_api_v1_workouts_proto_init() + file_api_v1_users_proto_msgTypes[14].OneofWrappers = []any{ + (*Notification_WorkoutComment_)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_api_v1_users_proto_rawDesc, NumEnums: 0, - NumMessages: 12, + NumMessages: 16, NumExtensions: 0, NumServices: 1, }, diff --git a/server/pkg/repo/helpers.go b/server/pkg/repo/helpers.go index 9c16d08c..c8059c71 100644 --- a/server/pkg/repo/helpers.go +++ b/server/pkg/repo/helpers.go @@ -9,7 +9,7 @@ import ( ) type ModelItem interface { - *orm.Workout | *orm.Exercise | *orm.User | *orm.Routine | *orm.Set | *orm.WorkoutComment + *orm.Workout | *orm.Exercise | *orm.User | *orm.Routine | *orm.Set | *orm.WorkoutComment | *orm.Notification } type ModelSlice[T any] interface { diff --git a/server/pkg/repo/repo.go b/server/pkg/repo/repo.go index 9304f92f..1cd3ea61 100644 --- a/server/pkg/repo/repo.go +++ b/server/pkg/repo/repo.go @@ -952,3 +952,142 @@ func (r *Repo) StoreTrace(ctx context.Context, p StoreTraceParams) error { return nil } + +type CreateNotificationParams struct { + Type orm.NotificationType + UserID string + Payload NotificationPayload +} + +type NotificationPayload struct { + ActorID string `json:"actorId,omitempty"` + WorkoutID string `json:"workoutId,omitempty"` +} + +func (r *Repo) CreateNotification(ctx context.Context, p CreateNotificationParams) error { + payload, err := json.Marshal(p.Payload) + if err != nil { + return fmt.Errorf("payload marshal: %w", err) + } + + n := &orm.Notification{ + UserID: p.UserID, + Type: p.Type, + Payload: payload, + } + if err = n.Insert(ctx, r.executor(), boil.Infer()); err != nil { + return fmt.Errorf("insert: %w", err) + } + + return nil +} + +type GetWorkoutCommentOpt func() qm.QueryMod + +func GetWorkoutCommentWithID(id string) GetWorkoutCommentOpt { + return func() qm.QueryMod { + return orm.WorkoutCommentWhere.ID.EQ(id) + } +} + +func GetWorkoutCommentWithWorkout() GetWorkoutCommentOpt { + return func() qm.QueryMod { + return qm.Load(orm.WorkoutCommentRels.Workout) + } +} + +func (r *Repo) GetWorkoutComment(ctx context.Context, opts ...GetWorkoutCommentOpt) (*orm.WorkoutComment, error) { + query := make([]qm.QueryMod, 0, len(opts)) + for _, opt := range opts { + query = append(query, opt()) + } + + comment, err := orm.WorkoutComments(query...).One(ctx, r.executor()) + if err != nil { + return nil, fmt.Errorf("workout comment fetch: %w", err) + } + + return comment, nil +} + +type ListNotificationsOpt func() qm.QueryMod + +func ListNotificationsWithLimit(limit int) ListNotificationsOpt { + return func() qm.QueryMod { + return qm.Limit(limit) + } +} + +func ListNotificationsWithUserID(userID string) ListNotificationsOpt { + return func() qm.QueryMod { + return orm.NotificationWhere.UserID.EQ(userID) + } +} + +func ListNotificationsWithOnlyUnread(onlyUnread bool) ListNotificationsOpt { + return func() qm.QueryMod { + if !onlyUnread { + return nil + } + + return orm.NotificationWhere.ReadAt.IsNull() + } +} + +func (r *Repo) ListNotifications(ctx context.Context, opts ...ListNotificationsOpt) (orm.NotificationSlice, error) { + query := make([]qm.QueryMod, 0, len(opts)) + for _, opt := range opts { + if opt() == nil { + continue + } + query = append(query, opt()) + } + + notifications, err := orm.Notifications(query...).All(ctx, r.executor()) + if err != nil { + return nil, fmt.Errorf("notifications fetch: %w", err) + } + + return notifications, nil +} + +type CountNotificationsOpt func() qm.QueryMod + +func CountNotificationsWithUserID(userID string) CountNotificationsOpt { + return func() qm.QueryMod { + return orm.NotificationWhere.UserID.EQ(userID) + } +} + +func CountNotificationsWithOnlyUnread(onlyUnread bool) CountNotificationsOpt { + return func() qm.QueryMod { + if !onlyUnread { + return nil + } + + return orm.NotificationWhere.ReadAt.IsNull() + } +} + +func (r *Repo) CountNotifications(ctx context.Context, opts ...CountNotificationsOpt) (int64, error) { + query := make([]qm.QueryMod, 0, len(opts)) + for _, opt := range opts { + if opt() == nil { + continue + } + query = append(query, opt()) + } + + count, err := orm.Notifications(query...).Count(ctx, r.executor()) + if err != nil { + return 0, fmt.Errorf("notifications count: %w", err) + } + + return count, nil +} + +func ListNotificationsOrderByCreatedAtDESC() ListNotificationsOpt { + return func() qm.QueryMod { + return qm.OrderBy(fmt.Sprintf("%s DESC", orm.NotificationColumns.CreatedAt)) + } +} diff --git a/server/rpc/v1/parser.go b/server/rpc/v1/parser.go index 57e529be..ac0055a9 100644 --- a/server/rpc/v1/parser.go +++ b/server/rpc/v1/parser.go @@ -225,3 +225,69 @@ func parseUserSliceToPB(users orm.UserSlice) []*apiv1.User { return pbUsers } + +func parseNotificationSliceToPB( + notifications orm.NotificationSlice, + payload map[string]repo.NotificationPayload, + users orm.UserSlice, + workouts orm.WorkoutSlice, +) []*apiv1.Notification { + mapWorkouts := make(map[string]*orm.Workout) + for _, w := range workouts { + mapWorkouts[w.ID] = w + } + + mapUsers := make(map[string]*orm.User) + for _, u := range users { + mapUsers[u.ID] = u + } + + var slice []*apiv1.Notification //nolint:prealloc + for _, n := range notifications { + p, ok := payload[n.ID] + if !ok { + continue + } + + w, ok := mapWorkouts[p.WorkoutID] + if !ok { + continue + } + + u, ok := mapUsers[p.ActorID] + if !ok { + continue + } + + slice = append(slice, parseNotificationToPB(n, u, w)) + } + + return slice +} + +func parseNotificationToPB(n *orm.Notification, u *orm.User, w *orm.Workout) *apiv1.Notification { + switch n.Type { + case orm.NotificationTypeFollow: + return nil + case orm.NotificationTypeWorkoutComment: + return &apiv1.Notification{ + Id: n.ID, + NotifiedAtUnix: n.CreatedAt.Unix(), + Type: &apiv1.Notification_WorkoutComment_{ + WorkoutComment: &apiv1.Notification_WorkoutComment{ + Actor: &apiv1.User{ + Id: u.ID, + FirstName: u.FirstName, + LastName: u.LastName, + }, + Workout: &apiv1.Workout{ + Id: w.ID, + Name: w.Name, + }, + }, + }, + } + default: + return nil + } +} diff --git a/server/rpc/v1/user.go b/server/rpc/v1/user.go index 86e109ab..f96a15d1 100644 --- a/server/rpc/v1/user.go +++ b/server/rpc/v1/user.go @@ -2,6 +2,7 @@ package v1 import ( "context" + "encoding/json" "time" "connectrpc.com/connect" @@ -156,3 +157,79 @@ func (h *userHandler) ListFollowees(ctx context.Context, req *connect.Request[v1 }, }, nil } + +func (h *userHandler) ListNotifications(ctx context.Context, req *connect.Request[v1.ListNotificationsRequest]) (*connect.Response[v1.ListNotificationsResponse], error) { + log := xcontext.MustExtractLogger(ctx) + userID := xcontext.MustExtractUserID(ctx) + + total, err := h.repo.CountNotifications(ctx, + repo.CountNotificationsWithUserID(userID), + repo.CountNotificationsWithOnlyUnread(req.Msg.GetOnlyUnread()), + ) + if err != nil { + log.Error("failed to count notifications", zap.Error(err)) + return nil, connect.NewError(connect.CodeInternal, nil) + } + + limit := int(req.Msg.GetPagination().GetPageLimit()) + notifications, err := h.repo.ListNotifications(ctx, + repo.ListNotificationsWithLimit(limit+1), + repo.ListNotificationsWithUserID(userID), + repo.ListNotificationsWithOnlyUnread(req.Msg.GetOnlyUnread()), + repo.ListNotificationsOrderByCreatedAtDESC(), + ) + if err != nil { + log.Error("failed to list notifications", zap.Error(err)) + return nil, connect.NewError(connect.CodeInternal, nil) + } + + paginated, err := repo.PaginateSlice(notifications, limit, func(n *orm.Notification) time.Time { + return n.CreatedAt + }) + if err != nil { + log.Error("failed to paginate notifications", zap.Error(err)) + return nil, connect.NewError(connect.CodeInternal, nil) + } + + var userIDs []string + var workoutIDs []string + payloads := make(map[string]repo.NotificationPayload) + + for _, n := range paginated.Items { + var payload repo.NotificationPayload + if err = json.Unmarshal(n.Payload, &payload); err != nil { + log.Error("failed to unmarshal notification payload", zap.Error(err)) + return nil, connect.NewError(connect.CodeInternal, nil) + } + + payloads[n.ID] = payload + if payload.WorkoutID != "" { + workoutIDs = append(workoutIDs, payload.WorkoutID) + } + if payload.ActorID != "" { + userIDs = append(userIDs, payload.ActorID) + } + } + + users, err := h.repo.ListUsers(ctx, repo.ListUsersWithIDs(userIDs)) + if err != nil { + log.Error("failed to list users", zap.Error(err)) + return nil, connect.NewError(connect.CodeInternal, nil) + } + + workouts, err := h.repo.ListWorkouts(ctx, repo.ListWorkoutsWithIDs(workoutIDs)) + if err != nil { + log.Error("failed to list workouts", zap.Error(err)) + return nil, connect.NewError(connect.CodeInternal, nil) + } + + return &connect.Response[v1.ListNotificationsResponse]{ + Msg: &v1.ListNotificationsResponse{ + Notifications: parseNotificationSliceToPB(paginated.Items, payloads, users, workouts), + Pagination: &v1.PaginationResponse{ + TotalResults: total, + NextPageToken: paginated.NextPageToken, + }, + }, + }, nil +} diff --git a/server/rpc/v1/workout.go b/server/rpc/v1/workout.go index c8f72809..7c9a3a7f 100644 --- a/server/rpc/v1/workout.go +++ b/server/rpc/v1/workout.go @@ -7,6 +7,9 @@ import ( "connectrpc.com/connect" "go.uber.org/zap" + "github.com/crlssn/getstronger/server/bus" + "github.com/crlssn/getstronger/server/bus/events" + "github.com/crlssn/getstronger/server/bus/payloads" "github.com/crlssn/getstronger/server/pkg/orm" v1 "github.com/crlssn/getstronger/server/pkg/pb/api/v1" "github.com/crlssn/getstronger/server/pkg/pb/api/v1/apiv1connect" @@ -17,11 +20,12 @@ import ( var _ apiv1connect.WorkoutServiceHandler = (*workoutHandler)(nil) type workoutHandler struct { + bus *bus.Bus repo *repo.Repo } -func NewWorkoutHandler(r *repo.Repo) apiv1connect.WorkoutServiceHandler { - return &workoutHandler{r} +func NewWorkoutHandler(b *bus.Bus, r *repo.Repo) apiv1connect.WorkoutServiceHandler { + return &workoutHandler{b, r} } func (h *workoutHandler) Create(ctx context.Context, req *connect.Request[v1.CreateWorkoutRequest]) (*connect.Response[v1.CreateWorkoutResponse], error) { @@ -205,6 +209,13 @@ func (h *workoutHandler) PostComment(ctx context.Context, req *connect.Request[v return nil, connect.NewError(connect.CodeInternal, nil) } + if err = h.bus.Publish(events.WorkoutCommentPosted, &payloads.WorkoutCommentPosted{ + CommentID: comment.ID, + }); err != nil { + log.Error("failed to publish event", zap.Error(err)) + return nil, connect.NewError(connect.CodeInternal, nil) + } + log.Info("workout comment posted") return &connect.Response[v1.PostCommentResponse]{ Msg: &v1.PostCommentResponse{ diff --git a/web/src/proto/api/v1/users_pb.ts b/web/src/proto/api/v1/users_pb.ts index d74be878..5f886051 100644 --- a/web/src/proto/api/v1/users_pb.ts +++ b/web/src/proto/api/v1/users_pb.ts @@ -7,6 +7,9 @@ import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1 import { file_api_v1_options } from "./options_pb"; import type { PaginationRequest, PaginationResponse, User } from "./shared_pb"; import { file_api_v1_shared } from "./shared_pb"; +import type { Workout } from "./workouts_pb"; +import { file_api_v1_workouts } from "./workouts_pb"; +import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; import { file_buf_validate_validate } from "../../buf/validate/validate_pb"; import type { Message } from "@bufbuild/protobuf"; @@ -14,7 +17,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file api/v1/users.proto. */ export const file_api_v1_users: GenFile = /*@__PURE__*/ - fileDesc("ChJhcGkvdjEvdXNlcnMucHJvdG8SBmFwaS52MSImCg5HZXRVc2VyUmVxdWVzdBIUCgJpZBgBIAEoCUIIukgFcgOwAQEiLQoPR2V0VXNlclJlc3BvbnNlEhoKBHVzZXIYASABKAsyDC5hcGkudjEuVXNlciIsCg1Gb2xsb3dSZXF1ZXN0EhsKCWZvbGxvd19pZBgBIAEoCUIIukgFcgOwAQEiEAoORm9sbG93UmVzcG9uc2UiMAoPVW5mb2xsb3dSZXF1ZXN0Eh0KC3VuZm9sbG93X2lkGAEgASgJQgi6SAVyA7ABASISChBVbmZvbGxvd1Jlc3BvbnNlIjUKFExpc3RGb2xsb3dlcnNSZXF1ZXN0Eh0KC2ZvbGxvd2VyX2lkGAEgASgJQgi6SAVyA7ABASI4ChVMaXN0Rm9sbG93ZXJzUmVzcG9uc2USHwoJZm9sbG93ZXJzGAEgAygLMgwuYXBpLnYxLlVzZXIiNQoUTGlzdEZvbGxvd2Vlc1JlcXVlc3QSHQoLZm9sbG93ZWVfaWQYASABKAlCCLpIBXIDsAEBIjgKFUxpc3RGb2xsb3dlZXNSZXNwb25zZRIfCglmb2xsb3dlZXMYASADKAsyDC5hcGkudjEuVXNlciJeCg1TZWFyY2hSZXF1ZXN0EhYKBXF1ZXJ5GAEgASgJQge6SARyAhADEjUKCnBhZ2luYXRpb24YAiABKAsyGS5hcGkudjEuUGFnaW5hdGlvblJlcXVlc3RCBrpIA8gBASJdCg5TZWFyY2hSZXNwb25zZRIbCgV1c2VycxgBIAMoCzIMLmFwaS52MS5Vc2VyEi4KCnBhZ2luYXRpb24YAiABKAsyGi5hcGkudjEuUGFnaW5hdGlvblJlc3BvbnNlMrYDCgtVc2VyU2VydmljZRI8CgNHZXQSFi5hcGkudjEuR2V0VXNlclJlcXVlc3QaFy5hcGkudjEuR2V0VXNlclJlc3BvbnNlIgSItRgBEj0KBkZvbGxvdxIVLmFwaS52MS5Gb2xsb3dSZXF1ZXN0GhYuYXBpLnYxLkZvbGxvd1Jlc3BvbnNlIgSItRgBEkMKCFVuZm9sbG93EhcuYXBpLnYxLlVuZm9sbG93UmVxdWVzdBoYLmFwaS52MS5VbmZvbGxvd1Jlc3BvbnNlIgSItRgBElIKDUxpc3RGb2xsb3dlcnMSHC5hcGkudjEuTGlzdEZvbGxvd2Vyc1JlcXVlc3QaHS5hcGkudjEuTGlzdEZvbGxvd2Vyc1Jlc3BvbnNlIgSItRgBElIKDUxpc3RGb2xsb3dlZXMSHC5hcGkudjEuTGlzdEZvbGxvd2Vlc1JlcXVlc3QaHS5hcGkudjEuTGlzdEZvbGxvd2Vlc1Jlc3BvbnNlIgSItRgBEj0KBlNlYXJjaBIVLmFwaS52MS5TZWFyY2hSZXF1ZXN0GhYuYXBpLnYxLlNlYXJjaFJlc3BvbnNlIgSItRgBQosBCgpjb20uYXBpLnYxQgpVc2Vyc1Byb3RvUAFaOGdpdGh1Yi5jb20vY3Jsc3NuL2dldHN0cm9uZ2VyL3NlcnZlci9wa2cvcGIvYXBpL3YxO2FwaXYxogIDQVhYqgIGQXBpLlYxygIGQXBpXFYx4gISQXBpXFYxXEdQQk1ldGFkYXRh6gIHQXBpOjpWMWIGcHJvdG8z", [file_api_v1_options, file_api_v1_shared, file_buf_validate_validate]); + fileDesc("ChJhcGkvdjEvdXNlcnMucHJvdG8SBmFwaS52MSImCg5HZXRVc2VyUmVxdWVzdBIUCgJpZBgBIAEoCUIIukgFcgOwAQEiLQoPR2V0VXNlclJlc3BvbnNlEhoKBHVzZXIYASABKAsyDC5hcGkudjEuVXNlciIsCg1Gb2xsb3dSZXF1ZXN0EhsKCWZvbGxvd19pZBgBIAEoCUIIukgFcgOwAQEiEAoORm9sbG93UmVzcG9uc2UiMAoPVW5mb2xsb3dSZXF1ZXN0Eh0KC3VuZm9sbG93X2lkGAEgASgJQgi6SAVyA7ABASISChBVbmZvbGxvd1Jlc3BvbnNlIjUKFExpc3RGb2xsb3dlcnNSZXF1ZXN0Eh0KC2ZvbGxvd2VyX2lkGAEgASgJQgi6SAVyA7ABASI4ChVMaXN0Rm9sbG93ZXJzUmVzcG9uc2USHwoJZm9sbG93ZXJzGAEgAygLMgwuYXBpLnYxLlVzZXIiNQoUTGlzdEZvbGxvd2Vlc1JlcXVlc3QSHQoLZm9sbG93ZWVfaWQYASABKAlCCLpIBXIDsAEBIjgKFUxpc3RGb2xsb3dlZXNSZXNwb25zZRIfCglmb2xsb3dlZXMYASADKAsyDC5hcGkudjEuVXNlciJeCg1TZWFyY2hSZXF1ZXN0EhYKBXF1ZXJ5GAEgASgJQge6SARyAhADEjUKCnBhZ2luYXRpb24YAiABKAsyGS5hcGkudjEuUGFnaW5hdGlvblJlcXVlc3RCBrpIA8gBASJdCg5TZWFyY2hSZXNwb25zZRIbCgV1c2VycxgBIAMoCzIMLmFwaS52MS5Vc2VyEi4KCnBhZ2luYXRpb24YAiABKAsyGi5hcGkudjEuUGFnaW5hdGlvblJlc3BvbnNlImYKGExpc3ROb3RpZmljYXRpb25zUmVxdWVzdBITCgtvbmx5X3VucmVhZBgBIAEoCBI1CgpwYWdpbmF0aW9uGAIgASgLMhkuYXBpLnYxLlBhZ2luYXRpb25SZXF1ZXN0Qga6SAPIAQEieAoZTGlzdE5vdGlmaWNhdGlvbnNSZXNwb25zZRIrCg1ub3RpZmljYXRpb25zGAEgAygLMhQuYXBpLnYxLk5vdGlmaWNhdGlvbhIuCgpwYWdpbmF0aW9uGAIgASgLMhouYXBpLnYxLlBhZ2luYXRpb25SZXNwb25zZSLNAQoMTm90aWZpY2F0aW9uEgoKAmlkGAEgASgJEhgKEG5vdGlmaWVkX2F0X3VuaXgYAiABKAMSPgoPd29ya291dF9jb21tZW50GAMgASgLMiMuYXBpLnYxLk5vdGlmaWNhdGlvbi5Xb3Jrb3V0Q29tbWVudEgAGk8KDldvcmtvdXRDb21tZW50EhsKBWFjdG9yGAEgASgLMgwuYXBpLnYxLlVzZXISIAoHd29ya291dBgCIAEoCzIPLmFwaS52MS5Xb3Jrb3V0QgYKBHR5cGUylgQKC1VzZXJTZXJ2aWNlEjwKA0dldBIWLmFwaS52MS5HZXRVc2VyUmVxdWVzdBoXLmFwaS52MS5HZXRVc2VyUmVzcG9uc2UiBIi1GAESPQoGRm9sbG93EhUuYXBpLnYxLkZvbGxvd1JlcXVlc3QaFi5hcGkudjEuRm9sbG93UmVzcG9uc2UiBIi1GAESQwoIVW5mb2xsb3cSFy5hcGkudjEuVW5mb2xsb3dSZXF1ZXN0GhguYXBpLnYxLlVuZm9sbG93UmVzcG9uc2UiBIi1GAESUgoNTGlzdEZvbGxvd2VycxIcLmFwaS52MS5MaXN0Rm9sbG93ZXJzUmVxdWVzdBodLmFwaS52MS5MaXN0Rm9sbG93ZXJzUmVzcG9uc2UiBIi1GAESUgoNTGlzdEZvbGxvd2VlcxIcLmFwaS52MS5MaXN0Rm9sbG93ZWVzUmVxdWVzdBodLmFwaS52MS5MaXN0Rm9sbG93ZWVzUmVzcG9uc2UiBIi1GAESPQoGU2VhcmNoEhUuYXBpLnYxLlNlYXJjaFJlcXVlc3QaFi5hcGkudjEuU2VhcmNoUmVzcG9uc2UiBIi1GAESXgoRTGlzdE5vdGlmaWNhdGlvbnMSIC5hcGkudjEuTGlzdE5vdGlmaWNhdGlvbnNSZXF1ZXN0GiEuYXBpLnYxLkxpc3ROb3RpZmljYXRpb25zUmVzcG9uc2UiBIi1GAFCiwEKCmNvbS5hcGkudjFCClVzZXJzUHJvdG9QAVo4Z2l0aHViLmNvbS9jcmxzc24vZ2V0c3Ryb25nZXIvc2VydmVyL3BrZy9wYi9hcGkvdjE7YXBpdjGiAgNBWFiqAgZBcGkuVjHKAgZBcGlcVjHiAhJBcGlcVjFcR1BCTWV0YWRhdGHqAgdBcGk6OlYxYgZwcm90bzM", [file_api_v1_options, file_api_v1_shared, file_api_v1_workouts, file_google_protobuf_timestamp, file_buf_validate_validate]); /** * @generated from message api.v1.GetUserRequest @@ -222,6 +225,107 @@ export type SearchResponse = Message<"api.v1.SearchResponse"> & { export const SearchResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_api_v1_users, 11); +/** + * @generated from message api.v1.ListNotificationsRequest + */ +export type ListNotificationsRequest = Message<"api.v1.ListNotificationsRequest"> & { + /** + * @generated from field: bool only_unread = 1; + */ + onlyUnread: boolean; + + /** + * @generated from field: api.v1.PaginationRequest pagination = 2; + */ + pagination?: PaginationRequest; +}; + +/** + * Describes the message api.v1.ListNotificationsRequest. + * Use `create(ListNotificationsRequestSchema)` to create a new message. + */ +export const ListNotificationsRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_v1_users, 12); + +/** + * @generated from message api.v1.ListNotificationsResponse + */ +export type ListNotificationsResponse = Message<"api.v1.ListNotificationsResponse"> & { + /** + * @generated from field: repeated api.v1.Notification notifications = 1; + */ + notifications: Notification[]; + + /** + * @generated from field: api.v1.PaginationResponse pagination = 2; + */ + pagination?: PaginationResponse; +}; + +/** + * Describes the message api.v1.ListNotificationsResponse. + * Use `create(ListNotificationsResponseSchema)` to create a new message. + */ +export const ListNotificationsResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_v1_users, 13); + +/** + * @generated from message api.v1.Notification + */ +export type Notification = Message<"api.v1.Notification"> & { + /** + * @generated from field: string id = 1; + */ + id: string; + + /** + * DEBT: This should be a timestamp but the client is not able to parse it. + * + * @generated from field: int64 notified_at_unix = 2; + */ + notifiedAtUnix: bigint; + + /** + * @generated from oneof api.v1.Notification.type + */ + type: { + /** + * @generated from field: api.v1.Notification.WorkoutComment workout_comment = 3; + */ + value: Notification_WorkoutComment; + case: "workoutComment"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message api.v1.Notification. + * Use `create(NotificationSchema)` to create a new message. + */ +export const NotificationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_v1_users, 14); + +/** + * @generated from message api.v1.Notification.WorkoutComment + */ +export type Notification_WorkoutComment = Message<"api.v1.Notification.WorkoutComment"> & { + /** + * @generated from field: api.v1.User actor = 1; + */ + actor?: User; + + /** + * @generated from field: api.v1.Workout workout = 2; + */ + workout?: Workout; +}; + +/** + * Describes the message api.v1.Notification.WorkoutComment. + * Use `create(Notification_WorkoutCommentSchema)` to create a new message. + */ +export const Notification_WorkoutCommentSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_v1_users, 14, 0); + /** * @generated from service api.v1.UserService */ @@ -274,6 +378,14 @@ export const UserService: GenService<{ input: typeof SearchRequestSchema; output: typeof SearchResponseSchema; }, + /** + * @generated from rpc api.v1.UserService.ListNotifications + */ + listNotifications: { + methodKind: "unary"; + input: typeof ListNotificationsRequestSchema; + output: typeof ListNotificationsResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_api_v1_users, 0); diff --git a/web/src/router/router.ts b/web/src/router/router.ts index e1b95cc9..65dcc366 100644 --- a/web/src/router/router.ts +++ b/web/src/router/router.ts @@ -19,6 +19,7 @@ import WorkoutRoutine from '@/ui/workouts/WorkoutRoutine.vue' import CreateExercise from '@/ui/exercises/CreateExercise.vue' import UpdateExercise from '@/ui/exercises/UpdateExercise.vue' import { createRouter, createWebHistory, type Router } from 'vue-router' +import ListNotifications from '@/ui/notifications/ListNotifications.vue' const router: Router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -30,6 +31,13 @@ const router: Router = createRouter({ name: 'home', path: '/home', }, + { + beforeEnter: [auth], + component: ListNotifications, + meta: { title: 'Notifications' }, + name: 'list-notifications', + path: '/notifications', + }, { beforeEnter: [auth], component: ProfileView, diff --git a/web/src/ui/components/NavigationMobile.vue b/web/src/ui/components/NavigationMobile.vue index eae48cf4..5ab68d85 100644 --- a/web/src/ui/components/NavigationMobile.vue +++ b/web/src/ui/components/NavigationMobile.vue @@ -1,9 +1,14 @@ @@ -57,4 +84,7 @@ nav { @apply fixed w-full bottom-0 z-50 h-16 px-8 bg-white border-t-2 border-gray-200; @apply lg:hidden flex justify-between items-center; } +.badge { + @apply absolute left-3 bottom-2 bg-red-600 rounded-full px-2 py-1 text-xs font-medium text-white scale-75; +} diff --git a/web/src/ui/components/NotificationWorkoutComment.vue b/web/src/ui/components/NotificationWorkoutComment.vue new file mode 100644 index 00000000..c28bfa30 --- /dev/null +++ b/web/src/ui/components/NotificationWorkoutComment.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/web/src/ui/exercises/ListExercises.vue b/web/src/ui/exercises/ListExercises.vue index ced74ad2..e598b4d1 100644 --- a/web/src/ui/exercises/ListExercises.vue +++ b/web/src/ui/exercises/ListExercises.vue @@ -47,7 +47,7 @@ const fetchExercises = async () => {
  • +import type { PaginationRequest } from '@/proto/api/v1/shared_pb.ts' + +import { onMounted, ref } from 'vue' +import { create } from '@bufbuild/protobuf' +import { UserClient } from '@/clients/clients.ts' +import NotificationWorkoutComment from '@/ui/components/NotificationWorkoutComment.vue' +import { ListNotificationsRequestSchema, type Notification } from '@/proto/api/v1/users_pb.ts' + +const notifications = ref([] as Notification[]) +const pageToken = ref(new Uint8Array(0)) + +const fetchUnreadNotifications = async () => { + const req = create(ListNotificationsRequestSchema, { + onlyUnread: false, + pagination: { + pageLimit: 100, + pageToken: pageToken.value, + } as PaginationRequest, + }) + + const res = await UserClient.listNotifications(req) + notifications.value = [...notifications.value, ...res.notifications] + pageToken.value = res.pagination?.nextPageToken || new Uint8Array(0) + if (pageToken.value.length > 0) { + // TODO: Implement pagination. + await fetchUnreadNotifications() + } +} + +onMounted(() => { + fetchUnreadNotifications() +}) + + + + +