Backend: - Add repost_handler.go with full CRUD (create, boost, delete, report, trending, amplification analytics) - Add profile_layout_handler.go for profile widget layout persistence (GET/PUT) - Wire FeedAlgorithmService into main.go as 15-min background score refresh job - Fix follow_handler.go (broken interface, dead query pattern, naming conflict) - Add DB migration for reposts, repost_reports, profile_layouts, post_feed_scores tables - Add engagement count columns to posts table for feed algorithm - Remove stale Supabase comments from auth middleware - Delete cmd/supabase-migrate/ directory (legacy migration tool) Flutter: - Fix all repost_service.dart API paths (were doubling /api/ prefix against base URL) - Rename forceResetBrokenKeys() -> resetIdentityKeys() in E2EE services - Remove dead _forceResetBrokenKeys method from secure_chat_screen.dart - Implement _navigateToProfile(), _navigateToHashtag(), _navigateToUrl() in sojorn_rich_text.dart Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
112 lines
3 KiB
Go
112 lines
3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
type ProfileLayoutHandler struct {
|
|
db *pgxpool.Pool
|
|
}
|
|
|
|
func NewProfileLayoutHandler(db *pgxpool.Pool) *ProfileLayoutHandler {
|
|
return &ProfileLayoutHandler{db: db}
|
|
}
|
|
|
|
// GetProfileLayout — GET /profile/layout
|
|
func (h *ProfileLayoutHandler) GetProfileLayout(c *gin.Context) {
|
|
userID, _ := c.Get("user_id")
|
|
userIDStr := userID.(string)
|
|
|
|
var widgetsJSON []byte
|
|
var theme string
|
|
var accentColor, bannerImageURL *string
|
|
var updatedAt time.Time
|
|
|
|
err := h.db.QueryRow(c.Request.Context(), `
|
|
SELECT widgets, theme, accent_color, banner_image_url, updated_at
|
|
FROM profile_layouts
|
|
WHERE user_id = $1
|
|
`, userIDStr).Scan(&widgetsJSON, &theme, &accentColor, &bannerImageURL, &updatedAt)
|
|
|
|
if err != nil {
|
|
// No layout yet — return empty default
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"widgets": []interface{}{},
|
|
"theme": "default",
|
|
"accent_color": nil,
|
|
"banner_image_url": nil,
|
|
"updated_at": time.Now().Format(time.RFC3339),
|
|
})
|
|
return
|
|
}
|
|
|
|
var widgets interface{}
|
|
if err := json.Unmarshal(widgetsJSON, &widgets); err != nil {
|
|
widgets = []interface{}{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"widgets": widgets,
|
|
"theme": theme,
|
|
"accent_color": accentColor,
|
|
"banner_image_url": bannerImageURL,
|
|
"updated_at": updatedAt.Format(time.RFC3339),
|
|
})
|
|
}
|
|
|
|
// SaveProfileLayout — PUT /profile/layout
|
|
func (h *ProfileLayoutHandler) SaveProfileLayout(c *gin.Context) {
|
|
userID, _ := c.Get("user_id")
|
|
userIDStr := userID.(string)
|
|
|
|
var req struct {
|
|
Widgets interface{} `json:"widgets"`
|
|
Theme string `json:"theme"`
|
|
AccentColor *string `json:"accent_color"`
|
|
BannerImageURL *string `json:"banner_image_url"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if req.Theme == "" {
|
|
req.Theme = "default"
|
|
}
|
|
|
|
widgetsJSON, err := json.Marshal(req.Widgets)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid widgets format"})
|
|
return
|
|
}
|
|
|
|
now := time.Now()
|
|
_, err = h.db.Exec(c.Request.Context(), `
|
|
INSERT INTO profile_layouts (user_id, widgets, theme, accent_color, banner_image_url, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
ON CONFLICT (user_id) DO UPDATE SET
|
|
widgets = EXCLUDED.widgets,
|
|
theme = EXCLUDED.theme,
|
|
accent_color = EXCLUDED.accent_color,
|
|
banner_image_url = EXCLUDED.banner_image_url,
|
|
updated_at = EXCLUDED.updated_at
|
|
`, userIDStr, widgetsJSON, req.Theme, req.AccentColor, req.BannerImageURL, now)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save layout"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"widgets": req.Widgets,
|
|
"theme": req.Theme,
|
|
"accent_color": req.AccentColor,
|
|
"banner_image_url": req.BannerImageURL,
|
|
"updated_at": now.Format(time.RFC3339),
|
|
})
|
|
}
|