diff --git a/HANDOFF.md b/HANDOFF.md index 247cd53..22da774 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -64,7 +64,8 @@ kick kills sync+video, no relay memory growth. - `packages/core/lib/src/`: constants.dart (tuning: seek 300 ms, drift 5 s, slow 1.5 s @0.95, min_buffer 4 s, heartbeat 1 s, ping 5 s, - avatar 64 KiB, name 32, chat 500; dropAfter lives as RoomEngine ctor + avatar 128 KiB (base64 chars; any 128x128 PNG fits — raised 09-12 + from 64 KiB, which rejected normal PNGs), name 32, chat 500; dropAfter lives as RoomEngine ctor default, now 30 s), errors.dart, logging.dart, config.dart, protocol.dart (v4 schemas), media_source.dart, sync_engine.dart (RoomEngine + quorum in evaluate's HELD branch; PLAYING branch @@ -149,10 +150,9 @@ Client: 5 s room-silence ⇒ dead (clear room, warn, reconnect loop asks-leave, global focus-proof keys (Space/Tab/arrows/ESC/F, 350 ms throttle, ignored while typing; Tab traversal disabled on the session screen 09-12 — game-style, Tab toggles the sheet), single media field - (parseMediaInput + tests), avatar pipeline (128 px PNG as of - 09-12 uncommitted change, was JPEG — NOTE: server still - enforces 64 KiB in session.dart vs pipeline's 512 KiB check, - mismatch open, see commit), tap to + (parseMediaInput + tests), avatar pipeline (128 px PNG, keeps + transparency — JPEG did not; self-check measures encoded base64 + against RelayConstants, single source of truth), tap to pick, no path fields), Discord-like corner profile (connect only; welcome is title+Host/Join; edits apply next join — no rename intent on the wire), dialogs for terminal states. diff --git a/apps/desktop/lib/src/widgets/avatar_pipeline.dart b/apps/desktop/lib/src/widgets/avatar_pipeline.dart index f3697e1..1d23d86 100644 --- a/apps/desktop/lib/src/widgets/avatar_pipeline.dart +++ b/apps/desktop/lib/src/widgets/avatar_pipeline.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:image/image.dart' as img; +import 'package:sync_relay_core/core.dart'; Future encodeAvatar(String path) async { final trimmed = path.trim(); @@ -24,22 +25,18 @@ Future encodeAvatar(String path) async { if (decoded == null) { throw const FormatException('could not read that image — pick a PNG, JPG or WebP photo'); } - // final side = decoded.width < decoded.height ? decoded.width : decoded.height; - // final x = (decoded.width - side) ~/ 2; - // final y = (decoded.height - side) ~/ 2; - // final cropped = img.copyCrop(decoded, x: x, y: y, width: side, height: side); final resized = img.copyResize( decoded, width: 128, height: 128, maintainAspect: true, - // backgroundColor: img.ColorFloat16.rgba(0, 0, 0, 0), backgroundColor: img.PixelUndefined(), interpolation: img.Interpolation.cubic, ); final png = img.encodePng(resized); - if (resized.length > 512 * 1024) { + final b64 = base64Encode(png); + if (b64.length > RelayConstants.maxAvatarBytes) { throw const FormatException('that photo is still too large after shrinking — pick a smaller one'); } - return base64Encode(png); + return b64; } diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 913ca46..2c972be 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -73,7 +73,7 @@ silence-timeout + reconnect loop. Never run: shaper chaos suite ## Phase 6 — Identity & social (PARTIAL) -Shipped: names (32 cap), avatars (64 KiB, picker + crop), kick nudge +Shipped: names (32 cap), avatars (128 KiB cap, 128 px PNG picker, transparency kept), kick nudge + reason, feed attribution. Open: real chat pane/history (schema only), sounds swap-in (wiring + NoopSfxService ready; pick flutter_soloud or equivalent, Kenney/ObsydianX/akx sources in diff --git a/packages/core/lib/src/constants.dart b/packages/core/lib/src/constants.dart index 383e4ae..c67ac62 100644 --- a/packages/core/lib/src/constants.dart +++ b/packages/core/lib/src/constants.dart @@ -15,7 +15,13 @@ abstract class RelayConstants { static const int stateHeartbeatSecs = 1; static const int pingIntervalSecs = 5; - static const int maxAvatarBytes = 64 * 1024; + // Base64-char count of the avatar string, enforced at hello. Sized so + // ANY 128x128 pipeline PNG always fits (raw bitmap <= 64 KiB, worst-case + // PNG ~70 KiB raw ~= 95 KiB base64). PNG keeps transparency; JPEG did not. + // Trade: avatars ride in the 1 Hz room broadcast, so worst-case host + // uplink scales with this cap. Follow-up (not now): send avatar bytes + // once, reference by hash afterwards. + static const int maxAvatarBytes = 128 * 1024; static const int maxNameLength = 32; static const int maxChatLength = 500; } diff --git a/packages/server/test/session_test.dart b/packages/server/test/session_test.dart index c9ca0cb..9c746b3 100644 --- a/packages/server/test/session_test.dart +++ b/packages/server/test/session_test.dart @@ -43,6 +43,13 @@ void main() { expect(() => s.issueToken('alice', big), throwsA(isA())); }); + test('64 KiB avatar accepted under raised cap', () { + final s = Session(id: 'default', secret: 'shh'); + final png = 'x' * (64 * 1024); + expect(s.issueToken('alice', png), isNotEmpty); + expect(s.tokens.values.single.avatar, hasLength(64 * 1024)); + }); + test('names truncated to cap, blank becomes ?', () { final s = Session(id: 'default', secret: 'shh'); final long = s.issueToken('a' * 100, '');