package model import "time" // Post is a public quote on a user's feed. Caption is the verbatim selected // text from the knowledgebase; SourceURL links back to the page the user // was reading. Images are loaded separately via PostImage rows. // // Reshares are real Post rows owned by the resharer, with // ResharedFromPostID pointing to the canonical original. Likes, comments, // and images all hang off the post's own id, so they diverge naturally // across reshare boundaries — no parallel "per-resharer" tables. type Post struct { ID string `json:"id"` UserID string `json:"user_id"` Caption string `json:"caption"` SourceURL string `json:"source_url"` SourceTitle string `json:"source_title"` Images []PostImage `json:"images"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` // IsHidden — owner has hidden this post from timelines (their profile as // seen by others, the home feed) and from public collections. It still // shows to the owner on their own profile and in private collections. IsHidden bool `json:"is_hidden"` // Author info — populated by repo joins for feed listings. AuthorName string `json:"author_name,omitempty"` AuthorHandle string `json:"author_handle,omitempty"` AuthorAvatarURL *string `json:"author_avatar_url,omitempty"` // Reshare attribution. ResharedFromPostID is non-nil when this post // is a reshare; the four `Original*` fields carry the canonical // post's author info so the frontend can render a // "Reshared from @handle" link without an extra round-trip. ResharedFromPostID *string `json:"reshared_from_post_id,omitempty"` OriginalAuthorName *string `json:"original_author_name,omitempty"` OriginalAuthorHandle *string `json:"original_author_handle,omitempty"` OriginalAuthorAvatar *string `json:"original_author_avatar_url,omitempty"` // ViewerHasReshared is true when the calling user has reshared the // canonical version of this post. Hydrated by the service layer // after the repo returns posts; always false for anonymous viewers. // Drives the post-card "Reshare ↔ Send" swap on the frontend. ViewerHasReshared bool `json:"viewer_has_reshared"` // Likes. LikesCount is the effective total = the denormalized // posts.likes_count plus any still-pending Redis delta (hydrated by the // like layer). ViewerHasLiked reflects the calling user's membership row; // false for anonymous viewers. LikesCount int `json:"likes_count"` ViewerHasLiked bool `json:"viewer_has_liked"` // MentionsCount is how many users are tagged on this post (drives the // post-card mentions icon). Hydrated by the mention layer. MentionsCount int `json:"mentions_count"` // PinnedComment is the post's pinned comment (if any), hydrated by the // comment layer so the card footer can show it without a per-card fetch. // Nil when nothing is pinned. PinnedComment *Comment `json:"pinned_comment,omitempty"` // CommentsCount is the total number of comments + replies on the post // (drives the footer's comment count). Hydrated by the comment layer. CommentsCount int `json:"comments_count"` // Viewer↔author relationship, hydrated for the home feed so the card can // render a Follow / Follow Back affordance. ViewerFollowsAuthor: the // caller follows (accepted) this post's author. AuthorFollowsViewer: the // author follows the caller. Both false for anonymous viewers / own posts. ViewerFollowsAuthor bool `json:"viewer_follows_author"` AuthorFollowsViewer bool `json:"author_follows_viewer"` } // Comment is one comment (or reply) on a post. Threading is one level deep: // ParentID is nil for a top-level comment and otherwise points at the thread // root. ReplyToHandle, when set, is the @handle the reply was addressed to // (replies stay flat under the root; this just preserves who was answered). type Comment struct { ID string `json:"id"` PostID string `json:"post_id"` UserID string `json:"user_id"` ParentID *string `json:"parent_id,omitempty"` Body string `json:"body"` LikesCount int `json:"likes_count"` PinnedAt *time.Time `json:"pinned_at,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` // Author info — populated by repo joins. AuthorName string `json:"author_name"` AuthorHandle string `json:"author_handle"` AuthorAvatarURL *string `json:"author_avatar_url,omitempty"` // ReplyToHandle is the handle this reply was addressed to (nil on // top-level comments), for the "Replying to @handle" prefix. ReplyToHandle *string `json:"reply_to_handle,omitempty"` // ViewerHasLiked reflects the calling user's like membership; false for // anonymous viewers. ReplyCount is the number of replies under a // top-level comment (0 on replies themselves). ViewerHasLiked bool `json:"viewer_has_liked"` ReplyCount int `json:"reply_count"` } // PostImage represents one attached image on a post. type PostImage struct { ID string `json:"id"` URL string `json:"url"` Position int `json:"position"` } // Bookmark is a private "save my place" marker. type Bookmark struct { ID string `json:"id"` UserID string `json:"user_id"` SourceURL string `json:"source_url"` SourceTitle string `json:"source_title"` Excerpt *string `json:"excerpt,omitempty"` Note *string `json:"note,omitempty"` CreatedAt time.Time `json:"created_at"` } // CollectionRecord is a user-curated group of posts. // // Renamed to avoid colliding with the legacy `Collection` model in user.go // from when bookmarks/collections were sketched out earlier. Once that old // model is unused we can drop it; for now this keeps both compiling. type CollectionRecord struct { ID string `json:"id"` UserID string `json:"user_id"` Title string `json:"title"` Description *string `json:"description,omitempty"` CoverURL *string `json:"cover_url,omitempty"` IsPublic bool `json:"is_public"` ItemCount int `json:"item_count"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // PublicProfile is what /u/{handle} returns — the parts of a User safe to // expose to anyone, plus aggregate counts and the user's posts. type PublicProfile struct { Handle string `json:"handle"` Name string `json:"name"` AvatarURL *string `json:"avatar_url"` JoinedAt time.Time `json:"joined_at"` PostCount int `json:"post_count"` BookmarkCount int `json:"-"` // never serialized — private } // RelationshipState is the viewer's relationship to a target profile, // computed fresh per request from the follows + blocks tables. Every // field is "from the viewer looking at the target": // // - Following: viewer → target follow edge is accepted. // - FollowRequested: viewer → target follow edge is pending (awaiting // the target's approval). The "Requested" button state. // - FollowsYou: target → viewer follow edge is accepted (they // already follow the viewer). Drives the "Follow back" label and // the "Remove from followers" menu item. // - RequestsYou: target → viewer follow edge is pending (the target // asked to follow the viewer). Drives the Accept / Reject buttons. // - Blocked: viewer has blocked the target. // - BlockedBy: target has blocked the viewer. // // nil (not this struct's zero value) is returned for anonymous viewers // and for a user viewing their own profile — callers special-case those. type RelationshipState struct { Following bool `json:"following"` FollowRequested bool `json:"follow_requested"` FollowsYou bool `json:"follows_you"` RequestsYou bool `json:"requests_you"` Blocked bool `json:"blocked"` BlockedBy bool `json:"blocked_by"` } // Follow is one row of the follows table — a directed, request-based // edge from Follower to Following. Status is "pending" until the target // accepts. type Follow struct { ID string `json:"id"` FollowerID string `json:"follower_id"` FollowingID string `json:"following_id"` Status string `json:"status"` CreatedAt time.Time `json:"created_at"` RespondedAt *time.Time `json:"responded_at,omitempty"` } // UserBlock is one row of user_blocks — Blocker has blocked Blocked. type UserBlock struct { ID string `json:"id"` BlockerID string `json:"blocker_id"` BlockedID string `json:"blocked_id"` CreatedAt time.Time `json:"created_at"` } // UserReport is one row of user_reports — Reporter flagged Reported. type UserReport struct { ID string `json:"id"` ReporterID string `json:"reporter_id"` ReportedID string `json:"reported_id"` Category string `json:"category"` Reason string `json:"reason"` CreatedAt time.Time `json:"created_at"` ResolvedAt *time.Time `json:"resolved_at,omitempty"` } // PostReport is one row of post_reports — Reporter flagged a specific // post. Reason is optional ("" when none given). type PostReport struct { ID string `json:"id"` ReporterID string `json:"reporter_id"` PostID string `json:"post_id"` Category string `json:"category"` Reason string `json:"reason"` CreatedAt time.Time `json:"created_at"` ResolvedAt *time.Time `json:"resolved_at,omitempty"` } // Notification is one delivered/queued notification for a recipient // (UserID). ActorID is who triggered it; Kind is a short machine string. // Hydrated Actor* fields are filled by repo joins for list rendering. type Notification struct { ID string `json:"id"` UserID string `json:"user_id"` ActorID *string `json:"actor_id,omitempty"` Kind string `json:"kind"` CreatedAt time.Time `json:"created_at"` ReadAt *time.Time `json:"read_at,omitempty"` // Aggregation: for post_like, PostID is the liked post and ActorCount is // how many likers have aggregated into this single row (the frontend // renders "X and N-1 others liked your post"). Other kinds: PostID nil, // ActorCount 1. PostID *string `json:"post_id,omitempty"` ActorCount int `json:"actor_count"` ActorName *string `json:"actor_name,omitempty"` ActorHandle *string `json:"actor_handle,omitempty"` ActorAvatar *string `json:"actor_avatar_url,omitempty"` // PostAuthorHandle is the handle of the author of PostID (when set), so // the client can deep-link to the post for kinds where the actor isn't // the post owner (e.g. comment_reply). Nil when there's no post. PostAuthorHandle *string `json:"post_author_handle,omitempty"` // Body carries free text for the 'admin_message' kind (an admin → user // direct message). Nil for every event-driven kind. Body *string `json:"body,omitempty"` } // PushSubscription is one browser/device Web Push endpoint a user granted. type PushSubscription struct { ID string `json:"id"` UserID string `json:"user_id"` Endpoint string `json:"endpoint"` P256dh string `json:"p256dh"` Auth string `json:"auth"` }