Private rooms were 100% client-simulated (the "friend" auto-accepted then bots
filled invited seats). Now they're server-authoritative over SignalR:
Server (GameManager.PrivateRooms + GameHub):
- Room registry with create/invite/accept/decline/addBot/clearSeat/start/leave.
- Invite pushes a `roomInvite` to that user (Clients.User); the seat stays
"invited" (a pending guest with their real profile, resolved server-side) — it
is NEVER replaced by a bot.
- StartPrivate refuses while any invite is pending; only EMPTY seats fill with
bots. Then it spins up a live GameRoom and matchFound → both devices enter.
- Host leave / disconnect closes the room (roomClosed); members free their seat.
Client:
- signalr-service implements the room methods over the hub (+ room/roomInvite/
roomClosed events, room mapping, onRoomInvite); mock keeps offline no-ops.
- online-store accept/declineInvite; RoomScreen blocks "Start" while an invite
is pending and auto-enters the live game on matchFound (host + friend).
- New global InviteModal (accept/decline) + i18n (invite.*, room.waitAccept).
Addresses: (1) no bot replacement, (2) game waits for acceptance, (3) invited
friend shown as a pending guest with their name/avatar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PostMatchRewardsModal: short-height (landscape) compaction so the win/forfeit
result fits without overflow (smaller emoji/coins/padding, max-h 94dvh, wider).
- Chat: emoji/sticker picker (owned reactions) — tap to send; hidden on focus.
- Unread messages: online-store now tracks a total `unread` (from
listConversations); NavRail Friends icon shows a badge (unread + requests),
refreshed every 12s on every screen; Friends «پیامها» tab badged too.
(Per-conversation unread badges already existed.)
- Remove "XP گران است" / "XP is expensive" from shop.xpHint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>