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
@@ -0,0 +1 @@
DROP TABLE IF EXISTS user_avatar_objects;
@@ -0,0 +1,12 @@
-- A user's avatar is stored as journaled objects bound by role, mirroring
-- photo_objects. Keeping the binding in the ledger means garbage collection
-- preserves avatar blobs while they are referenced and reclaims them once the
-- avatar is replaced or removed.
CREATE TABLE IF NOT EXISTS user_avatar_objects (
user_id text NOT NULL REFERENCES users(id) ON DELETE CASCADE,
object_id text NOT NULL REFERENCES objects(id) ON DELETE RESTRICT,
role text NOT NULL CHECK (role IN ('avatar', 'avatar_small')),
PRIMARY KEY (user_id, role)
);
CREATE INDEX IF NOT EXISTS user_avatar_objects_object_idx ON user_avatar_objects (object_id);
@@ -0,0 +1 @@
DROP TABLE IF EXISTS user_avatar_objects;
@@ -0,0 +1,12 @@
-- A user's avatar is stored as journaled objects bound by role, mirroring
-- photo_objects. Keeping the binding in the ledger means garbage collection
-- preserves avatar blobs while they are referenced and reclaims them once the
-- avatar is replaced or removed.
CREATE TABLE user_avatar_objects (
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
object_id TEXT NOT NULL REFERENCES objects(id) ON DELETE RESTRICT,
role TEXT NOT NULL CHECK (role IN ('avatar', 'avatar_small')),
PRIMARY KEY (user_id, role)
);
CREATE INDEX IF NOT EXISTS user_avatar_objects_object_idx ON user_avatar_objects (object_id);