feat(profile): add round avatar with upload and cropping

- backend: user_avatar_objects binding (PG+SQLite), GC preserves avatar blobs,
  avatar service (EXIF-stripping WebP 128/512), public avatar read route
- web: avatar upload/crop on profile, round avatar in header/members/comments/author
- android: avatar upload/crop, round avatar on profile and albums header
- openspec: add-user-avatar proposal/design/specs/tasks
This commit is contained in:
iu
2026-10-12 16:16:47 +03:00
parent 657a04309e
commit 04617d1cdb
49 changed files with 2362 additions and 21 deletions
+25
View File
@@ -28,3 +28,28 @@ func TestPublicUserOmitsPhone(t *testing.T) {
t.Fatalf("SelfUser phone not serialized: %s", self)
}
}
func TestPublicUserCarriesAvatar(t *testing.T) {
u := &User{ID: "u1", Name: "Tester", AvatarURL: "/api/avatars/u1/v123"}
pub, err := json.Marshal(u.Public())
if err != nil {
t.Fatalf("marshal public: %v", err)
}
if !strings.Contains(string(pub), `"avatar_url":"/api/avatars/u1/v123"`) {
t.Fatalf("PublicUser avatar_url missing: %s", pub)
}
self, err := json.Marshal(u.Self())
if err != nil {
t.Fatalf("marshal self: %v", err)
}
if !strings.Contains(string(self), `"avatar_url":"/api/avatars/u1/v123"`) {
t.Fatalf("SelfUser avatar_url missing: %s", self)
}
// Without an avatar the field is omitted, so clients fall back to a placeholder.
bare, _ := json.Marshal((&User{ID: "u2", Name: "Bare"}).Public())
if strings.Contains(string(bare), "avatar_url") {
t.Fatalf("empty avatar_url should be omitted: %s", bare)
}
}