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:
@@ -53,6 +53,8 @@ export class SignalrService implements OnlineService {
|
||||
private notifCbs = new Set<(n: AppNotification) => void>();
|
||||
private profileCbs = new Set<(p: UserProfile) => void>();
|
||||
private rewardCbs = new Set<(r: RewardResult) => void>();
|
||||
private friendCbs = new Set<(f: Friend[]) => void>();
|
||||
private chatCbs = new Set<(id: string, m: ChatMessage[]) => void>();
|
||||
private cachedProfile: UserProfile | null = null;
|
||||
private mockNotifUnsub?: () => void;
|
||||
|
||||
@@ -122,6 +124,23 @@ export class SignalrService implements OnlineService {
|
||||
this.profileCbs.forEach((cb) => cb(p));
|
||||
});
|
||||
conn.on("reward", (r: RewardResult) => this.rewardCbs.forEach((cb) => cb(r)));
|
||||
conn.on("friendRequest", (from: Friend) => {
|
||||
this.notifCbs.forEach((cb) =>
|
||||
cb({
|
||||
id: `n_fr_${from.id}`,
|
||||
kind: "friend_request",
|
||||
titleFa: "درخواست دوستی جدید",
|
||||
titleEn: "New friend request",
|
||||
bodyFa: `${from.displayName} میخواهد با شما دوست شود`,
|
||||
bodyEn: `${from.displayName} wants to be your friend`,
|
||||
icon: "👥",
|
||||
ts: Date.now(),
|
||||
read: false,
|
||||
} as AppNotification));
|
||||
void this.refreshFriends();
|
||||
});
|
||||
conn.on("social", () => void this.refreshFriends());
|
||||
conn.on("chat", (m: { peerId: string }) => void this.emitChat(m.peerId));
|
||||
|
||||
this.conn = conn;
|
||||
try {
|
||||
@@ -306,13 +325,33 @@ export class SignalrService implements OnlineService {
|
||||
return p;
|
||||
}
|
||||
|
||||
listFriends() { return this.mock.listFriends(); }
|
||||
listRequests() { return this.mock.listRequests(); }
|
||||
addFriend(q: string) { return this.mock.addFriend(q); }
|
||||
acceptRequest(id: string) { return this.mock.acceptRequest(id); }
|
||||
declineRequest(id: string) { return this.mock.declineRequest(id); }
|
||||
removeFriend(id: string) { return this.mock.removeFriend(id); }
|
||||
onFriends(cb: (f: Friend[]) => void) { return this.mock.onFriends(cb); }
|
||||
private async refreshFriends() {
|
||||
try {
|
||||
const f = await this.listFriends();
|
||||
this.friendCbs.forEach((cb) => cb(f));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
private async emitChat(peerId: string) {
|
||||
try {
|
||||
const m = await this.getMessages(peerId);
|
||||
this.chatCbs.forEach((cb) => cb(peerId, m));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
listFriends() { return this.getJson<Friend[]>("/api/friends"); }
|
||||
listRequests() { return this.getJson<FriendRequest[]>("/api/friends/requests"); }
|
||||
addFriend(q: string) {
|
||||
return this.send<{ ok: boolean; messageFa: string; messageEn: string }>(
|
||||
"POST", "/api/friends/add", { query: q });
|
||||
}
|
||||
async acceptRequest(id: string) { await this.send<unknown>("POST", "/api/friends/accept", { id }); }
|
||||
async declineRequest(id: string) { await this.send<unknown>("POST", "/api/friends/decline", { id }); }
|
||||
async removeFriend(id: string) { await this.send<unknown>("POST", "/api/friends/remove", { id }); }
|
||||
onFriends(cb: (f: Friend[]) => void) { this.friendCbs.add(cb); return () => this.friendCbs.delete(cb); }
|
||||
|
||||
createRoom(o: CreateRoomOptions) { return this.mock.createRoom(o); }
|
||||
setPartner(roomId: string, friendId: string | null) { return this.mock.setPartner(roomId, friendId); }
|
||||
@@ -323,11 +362,15 @@ export class SignalrService implements OnlineService {
|
||||
leaveRoom(roomId: string) { return this.mock.leaveRoom(roomId); }
|
||||
onRoom(cb: (r: Room) => void) { return this.mock.onRoom(cb); }
|
||||
|
||||
listConversations(): Promise<Conversation[]> { return this.mock.listConversations(); }
|
||||
getMessages(id: string): Promise<ChatMessage[]> { return this.mock.getMessages(id); }
|
||||
sendMessage(id: string, text: string) { return this.mock.sendMessage(id, text); }
|
||||
markRead(id: string) { return this.mock.markRead(id); }
|
||||
onChat(cb: (id: string, m: ChatMessage[]) => void) { return this.mock.onChat(cb); }
|
||||
listConversations(): Promise<Conversation[]> { return this.getJson<Conversation[]>("/api/chat"); }
|
||||
getMessages(id: string): Promise<ChatMessage[]> {
|
||||
return this.getJson<ChatMessage[]>(`/api/chat/messages?peer=${encodeURIComponent(id)}`);
|
||||
}
|
||||
sendMessage(id: string, text: string) {
|
||||
return this.send<ChatMessage>("POST", "/api/chat/send", { peerId: id, text });
|
||||
}
|
||||
async markRead() { /* server marks read when messages are fetched */ }
|
||||
onChat(cb: (id: string, m: ChatMessage[]) => void) { this.chatCbs.add(cb); return () => this.chatCbs.delete(cb); }
|
||||
|
||||
onNotification(cb: (n: AppNotification) => void): Unsubscribe {
|
||||
this.notifCbs.add(cb);
|
||||
|
||||
Reference in New Issue
Block a user