Skip to content
Merged

Dev #153

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
609d4a1
feat(chat): make ai image stickers beautiful
Kanye-Est Mar 12, 2026
5490f7e
feat(home): relocate ai image
Kanye-Est Mar 12, 2026
e542c1a
feat(chat): change photo wall stickers from grayscale to brightness-b…
Kanye-Est Mar 12, 2026
06f7ebe
style(car): unify photo detail modal visual style with profile modal
koishi510 Mar 12, 2026
d7a2d0a
fix: fix pearl shell position
koishi510 Mar 12, 2026
54f9758
fix(car): decouple pearl shell trigger zone from photo wall
koishi510 Mar 12, 2026
b387724
feat: relocate memory toast
Kanye-Est Mar 12, 2026
c6ee937
fix(car): adjust suitcase position to align with background ground
Kanye-Est Mar 12, 2026
e2076cd
fix: fix ai reference
koishi510 Mar 12, 2026
d485e75
feat: bloom in pearlshell
Kanye-Est Mar 12, 2026
4331142
fix: improve ai memory
koishi510 Mar 12, 2026
8622163
fix: fix ui
koishi510 Mar 12, 2026
b29e395
feat: pearls
Kanye-Est Mar 12, 2026
3949452
feat: pearls 2
Kanye-Est Mar 12, 2026
429a641
feat: pearls 3
Kanye-Est Mar 12, 2026
4d3591b
fix: update ui
koishi510 Mar 12, 2026
913e337
feat: pearls 4
Kanye-Est Mar 12, 2026
5175b0a
feat: stars
Kanye-Est Mar 12, 2026
578d0da
feat: stars 2
Kanye-Est Mar 12, 2026
3f3db64
feat: stars 3
Kanye-Est Mar 12, 2026
4ac0c21
feat: stars 4
Kanye-Est Mar 12, 2026
8f34e5c
fix: update ui
koishi510 Mar 12, 2026
2f0c423
feat: stars 5
Kanye-Est Mar 12, 2026
06384bc
feat: share photos and AI memories between bound partners
koishi510 Mar 12, 2026
744dc78
chore: update .gitignore
koishi510 Mar 12, 2026
a206cc7
fix: trigger memory update toast for fact saves and corrections
koishi510 Mar 12, 2026
4a52752
fix: dedup memory facts per-user instead of per-family
koishi510 Mar 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ archive-legacy-*.tar.gz
CLAUDE.md
PROGRESS.md
SKILL.md
PLAN.md

# =============================================================================
# Misc
Expand Down
3 changes: 3 additions & 0 deletions backend/internal/database/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ func Migrate(db *gorm.DB) error {
"role": "mom",
})

// Backfill OwnerUserID for existing ChatMemoryFacts
db.Exec("UPDATE chat_memory_facts SET owner_user_id = user_id WHERE owner_user_id IS NULL OR owner_user_id = ''")

return nil
}

Expand Down
2 changes: 2 additions & 0 deletions backend/internal/dto/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ type ChatMemoryFactDTO struct {
ID string `json:"id"`
Content string `json:"content"`
Category string `json:"category"`
OwnerUserID string `json:"owner_user_id"`
OwnerNickname string `json:"owner_nickname"`
CreatedAt string `json:"created_at"`
LastReferencedAt *string `json:"last_referenced_at"`
}
Expand Down
22 changes: 12 additions & 10 deletions backend/internal/dto/photo.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package dto

type PhotoResponse struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Tags []string `json:"tags"`
ImageURL string `json:"image_url"`
IsOnWall bool `json:"is_on_wall"`
WallPosition *int `json:"wall_position"`
Source string `json:"source"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Tags []string `json:"tags"`
ImageURL string `json:"image_url"`
IsOnWall bool `json:"is_on_wall"`
WallPosition *int `json:"wall_position"`
Source string `json:"source"`
OwnerID string `json:"owner_id"`
OwnerNickname string `json:"owner_nickname"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}

type PhotoListResponse struct {
Expand Down
1 change: 1 addition & 0 deletions backend/internal/model/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const (
type ChatMemoryFact struct {
ID string `gorm:"type:varchar(36);primaryKey" json:"id"`
UserID string `gorm:"type:varchar(36);index;not null" json:"user_id"`
OwnerUserID string `gorm:"type:varchar(36);index" json:"owner_user_id"`
Content string `gorm:"type:text;not null" json:"content"`
Category FactCategory `gorm:"type:varchar(30);default:'other'" json:"category"`
CreatedAt time.Time `json:"created_at"`
Expand Down
67 changes: 67 additions & 0 deletions backend/internal/repository/chat.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package repository

import (
"strings"
"time"

"github.com/momshell/backend/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
Expand Down Expand Up @@ -85,3 +88,67 @@ func (r *ChatRepo) TouchFactReferencedAt(ids []string) error {
Where("id IN ?", ids).
Update("last_referenced_at", gorm.Expr("NOW()")).Error
}

// FindDeletedFactsByUserID returns soft-deleted facts from the last 90 days.
func (r *ChatRepo) FindDeletedFactsByUserID(userID string) ([]model.ChatMemoryFact, error) {
var facts []model.ChatMemoryFact
cutoff := time.Now().AddDate(0, 0, -90)
err := r.db.Unscoped().
Where("user_id = ? AND deleted_at IS NOT NULL AND deleted_at > ?", userID, cutoff).
Find(&facts).Error
return facts, err
}

// DeleteFactsByContentLike soft-deletes facts whose content matches any of the given phrases.
func (r *ChatRepo) DeleteFactsByContentLike(userID string, phrases []string) error {
for _, phrase := range phrases {
phrase = strings.TrimSpace(phrase)
if phrase == "" {
continue
}
if err := r.db.Where("user_id = ? AND content LIKE ?", userID, "%"+phrase+"%").
Delete(&model.ChatMemoryFact{}).Error; err != nil {
return err
}
}
return nil
}

// --- Family (partner-shared) query methods ---

func (r *ChatRepo) FindFactsByFamilyIDs(familyIDs []string) ([]model.ChatMemoryFact, error) {
var facts []model.ChatMemoryFact
err := r.db.Where("user_id IN ?", familyIDs).Order("created_at desc").Find(&facts).Error
return facts, err
}

func (r *ChatRepo) FindDeletedFactsByFamilyIDs(familyIDs []string) ([]model.ChatMemoryFact, error) {
var facts []model.ChatMemoryFact
cutoff := time.Now().AddDate(0, 0, -90)
err := r.db.Unscoped().
Where("user_id IN ? AND deleted_at IS NOT NULL AND deleted_at > ?", familyIDs, cutoff).
Find(&facts).Error
return facts, err
}

func (r *ChatRepo) FactExistsByContentFamily(familyIDs []string, content string) (bool, error) {
var count int64
err := r.db.Unscoped().Model(&model.ChatMemoryFact{}).
Where("user_id IN ? AND content = ?", familyIDs, content).
Count(&count).Error
return count > 0, err
}

func (r *ChatRepo) DeleteFactsByContentLikeFamily(familyIDs []string, phrases []string) error {
for _, phrase := range phrases {
phrase = strings.TrimSpace(phrase)
if phrase == "" {
continue
}
if err := r.db.Where("user_id IN ? AND content LIKE ?", familyIDs, "%"+phrase+"%").
Delete(&model.ChatMemoryFact{}).Error; err != nil {
return err
}
}
return nil
}
66 changes: 66 additions & 0 deletions backend/internal/repository/photo.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,69 @@ func (r *PhotoRepo) FindAllPaginated(search, userID, source, onWall string, limi
err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&photos).Error
return photos, total, err
}

// --- Family (partner-shared) query methods ---

func (r *PhotoRepo) FindByFamilyIDs(familyIDs []string, limit, offset int) ([]model.Photo, int64, error) {
query := r.db.Model(&model.Photo{}).Where("user_id IN ?", familyIDs)

var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}

var photos []model.Photo
err := query.Order("created_at desc").Offset(offset).Limit(limit).Find(&photos).Error
return photos, total, err
}

func (r *PhotoRepo) FindWallPhotosByFamily(familyIDs []string) ([]model.Photo, error) {
var photos []model.Photo
err := r.db.Where("user_id IN ? AND is_on_wall = ?", familyIDs, true).
Order("wall_position asc").Find(&photos).Error
return photos, err
}

func (r *PhotoRepo) CountByFamilyIDs(familyIDs []string) (int64, error) {
var count int64
err := r.db.Model(&model.Photo{}).Where("user_id IN ?", familyIDs).Count(&count).Error
return count, err
}

func (r *PhotoRepo) CountWallPhotosByFamily(familyIDs []string) (int64, error) {
var count int64
err := r.db.Model(&model.Photo{}).Where("user_id IN ? AND is_on_wall = ?", familyIDs, true).Count(&count).Error
return count, err
}

func (r *PhotoRepo) FindByIDAndFamilyIDs(id string, familyIDs []string) (*model.Photo, error) {
var photo model.Photo
err := r.db.Where("id = ? AND user_id IN ?", id, familyIDs).First(&photo).Error
if err != nil {
return nil, err
}
return &photo, nil
}

func (r *PhotoRepo) BatchUpdateWallFamily(familyIDs []string, updates []WallUpdate) error {
return r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&model.Photo{}).
Where("user_id IN ? AND is_on_wall = ?", familyIDs, true).
Updates(map[string]any{"is_on_wall": false, "wall_position": nil}).Error; err != nil {
return err
}
for _, u := range updates {
pos := u.Position
if err := tx.Model(&model.Photo{}).
Where("id = ? AND user_id IN ?", u.PhotoID, familyIDs).
Updates(map[string]any{"is_on_wall": true, "wall_position": pos}).Error; err != nil {
return err
}
}
return nil
})
}

func (r *PhotoRepo) DeleteByFamily(id string, familyIDs []string) error {
return r.db.Where("id = ? AND user_id IN ?", id, familyIDs).Delete(&model.Photo{}).Error
}
Loading
Loading