253 lines
11 KiB
Go
253 lines
11 KiB
Go
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"`
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|