feat: share post to other apps

This commit is contained in:
Krispire Admin
2026-05-31 08:05:02 +05:30
parent 05b61cef09
commit 0969887008
8 changed files with 313 additions and 14 deletions
+110 -8
View File
@@ -61,9 +61,88 @@ func (s *PostService) Create(ctx context.Context, userID, caption, sourceURL, so
return s.posts.Create(ctx, userID, caption, sourceURL, sourceTitle, imageURLs)
}
// Get returns one post (public).
func (s *PostService) Get(ctx context.Context, id string) (*model.Post, error) {
return s.posts.FindByID(ctx, id)
// Get returns one post (public). `viewerID` is "" for anonymous callers;
// when set, the returned post carries the viewer's reshare state so the
// frontend can render the right action button without a second request.
func (s *PostService) Get(ctx context.Context, id, viewerID string) (*model.Post, error) {
p, err := s.posts.FindByID(ctx, id)
if err != nil {
return nil, err
}
if err := s.hydrateReshareStates(ctx, viewerID, []*model.Post{p}); err != nil {
return nil, err
}
return p, nil
}
// Reshare records that the caller has reshared the given post. Idempotent
// — calling it twice with the same (post, user) is a no-op. Resharing
// your own post is rejected: there's nothing to bring onto an account
// that already authored the post.
func (s *PostService) Reshare(ctx context.Context, postID, callerID string) error {
p, err := s.posts.FindByID(ctx, postID)
if err != nil {
return err
}
if p.UserID == callerID {
return errors.New("cannot reshare your own post")
}
return s.posts.AddReshare(ctx, postID, callerID)
}
// Unreshare removes a previously recorded reshare. Idempotent — no-ops
// when no row exists.
func (s *PostService) Unreshare(ctx context.Context, postID, callerID string) error {
return s.posts.RemoveReshare(ctx, postID, callerID)
}
// hydrateReshareStates fills in ViewerHasReshared on each post via one
// batched lookup against post_reshares. Skips the query when the viewer
// is anonymous (`viewerID == ""`) since the answer is uniformly false.
// Also fast-paths posts authored by the viewer — they can't be reshared
// by themselves, so we leave the flag at its zero value.
func (s *PostService) hydrateReshareStates(ctx context.Context, viewerID string, posts []*model.Post) error {
if viewerID == "" || len(posts) == 0 {
return nil
}
ids := make([]string, 0, len(posts))
for _, p := range posts {
if p == nil || p.UserID == viewerID {
continue
}
ids = append(ids, p.ID)
}
if len(ids) == 0 {
return nil
}
states, err := s.posts.ReshareStatesFor(ctx, viewerID, ids)
if err != nil {
return err
}
for _, p := range posts {
if p == nil {
continue
}
if states[p.ID] {
p.ViewerHasReshared = true
}
}
return nil
}
// hydrateReshareStatesSlice is the value-slice variant of
// hydrateReshareStates. Repository list methods return []model.Post
// (not pointers) — we wrap each element so the helper can mutate in
// place via a stable pointer.
func (s *PostService) hydrateReshareStatesSlice(ctx context.Context, viewerID string, posts []model.Post) error {
if len(posts) == 0 {
return nil
}
ptrs := make([]*model.Post, len(posts))
for i := range posts {
ptrs[i] = &posts[i]
}
return s.hydrateReshareStates(ctx, viewerID, ptrs)
}
// Delete checks ownership and removes the post.
@@ -81,19 +160,28 @@ func (s *PostService) ReplaceImages(ctx context.Context, postID, callerID string
return s.posts.FindByID(ctx, postID)
}
// ListByHandle is the public feed view.
func (s *PostService) ListByHandle(ctx context.Context, handle string, limit, offset int) ([]model.Post, int, error) {
// ListByHandle is the public feed view. `viewerID` is "" for anonymous
// callers; when set, each returned post carries the viewer's reshare
// state.
func (s *PostService) ListByHandle(ctx context.Context, handle, viewerID string, limit, offset int) ([]model.Post, int, error) {
if limit <= 0 || limit > 50 {
limit = 20
}
if offset < 0 {
offset = 0
}
return s.posts.ListByHandle(ctx, handle, limit, offset)
posts, total, err := s.posts.ListByHandle(ctx, handle, limit, offset)
if err != nil {
return nil, 0, err
}
if err := s.hydrateReshareStatesSlice(ctx, viewerID, posts); err != nil {
return nil, 0, err
}
return posts, total, nil
}
// ListMine is the owner's view of their own posts (drafts feature isn't a
// thing yet, so this is equivalent to ListByHandle minus the handle lookup).
// ListMine is the owner's view of their own posts. Reshare hydration
// is skipped — the caller can't reshare their own posts.
func (s *PostService) ListMine(ctx context.Context, userID string, limit, offset int) ([]model.Post, int, error) {
if limit <= 0 || limit > 50 {
limit = 20
@@ -336,6 +424,20 @@ func (s *CollectionService) Get(ctx context.Context, id, viewerID string) (*mode
for i := range posts {
posts[i].Images = imgs[posts[i].ID]
}
// Same batched approach for reshare states so the action footer
// on each card swaps Reshare↔Send correctly. Anonymous viewers
// (viewerID == "") get all-false at zero cost.
if viewerID != "" {
states, err := s.posts.ReshareStatesFor(ctx, viewerID, ids)
if err != nil {
return nil, nil, err
}
for i := range posts {
if states[posts[i].ID] {
posts[i].ViewerHasReshared = true
}
}
}
}
return c, posts, nil
}