Server-backed friends, chat, IAB scaffold + EF migrations/Postgres

- Social: EF-backed friends graph + chat (SocialService/SocialModels);
  REST endpoints (friends add/accept/decline/remove/list/requests,
  chat conversations/messages/send) with real-time hub events
  (friendRequest/social/chat). GameManager tracks online users for presence.
- Client SignalrService: friends + chat now hit the server and react to
  hub events (refetch + emit); no longer delegated to the mock.
- IAB: /api/coins/iab/verify endpoint + IabVerifyReq for Cafe Bazaar/Myket
  (token verification is a documented TODO pending store accounts/SKUs).
- Persistence: EF Core Design package + DesignTimeDbContextFactory (Postgres),
  Program auto-migrate/EnsureCreated, appsettings.Production.json.example
  with Supabase connection + live ZarinPal template.

Verified end-to-end (two users, SQLite dev): request -> accept ->
bidirectional friends, chat send with per-user fromMe, unread count +
read-on-fetch. Server + client builds clean (dotnet build, tsc, next build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 18:26:22 +03:30
parent cfed2950b2
commit e778e8b5bd
9 changed files with 381 additions and 17 deletions
+6 -4
View File
@@ -141,18 +141,20 @@ public sealed class GameManager
public void ChooseTrump(string userId, string suit) => RoomOf(userId)?.HumanChooseTrump(userId, suit);
public void SendReaction(string userId, string reaction) => RoomOf(userId)?.Reaction(userId, reaction);
private int _online;
public int OnlineCount => Volatile.Read(ref _online);
private readonly ConcurrentDictionary<string, int> _onlineUsers = new();
public int OnlineCount => _onlineUsers.Count;
public bool IsOnline(string userId) => _onlineUsers.ContainsKey(userId);
public void OnConnected(string userId)
{
Interlocked.Increment(ref _online);
_onlineUsers.AddOrUpdate(userId, 1, (_, n) => n + 1);
RoomOf(userId)?.SetConnected(userId, true);
}
public void OnDisconnected(string userId)
{
if (Interlocked.Decrement(ref _online) < 0) Interlocked.Exchange(ref _online, 0);
if (_onlineUsers.AddOrUpdate(userId, 0, (_, n) => n - 1) <= 0)
_onlineUsers.TryRemove(userId, out _);
CancelMatchmaking(userId);
RoomOf(userId)?.SetConnected(userId, false);
}