feat: photo upload at level 3 + report a player (nudity avatar / chat insult)
Photo upload:
- Lower the custom profile-photo gate from level 25 to level 3 (client const +
i18n hint + server gate in ProfileService.Update). The level-25 "Expert" title
is unrelated and unchanged.
Report a player:
- New ReportReason type + service.reportUser(targetId, reason, details?).
- Report entry points: a "گزارش تخلف" button + reason picker (nudity / insult /
other) in the public-profile modal, and a flag button in the chat header
(reports the peer for an insulting chat) with a confirmation toast.
- Mock records reports to localStorage; SignalR POSTs /api/report.
- Server: POST /api/report → ProfileService.ReportUser stores the report in the
write-only ledger (kind="report", ref="{targetId}|{reason}|{details}") so no
schema change is needed (server uses EnsureCreated, not migrations).
- i18n: report.* keys (fa + en).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+16
-2
@@ -42,7 +42,7 @@ const fa: Dict = {
|
||||
"achv.unlocksSticker": "استیکر",
|
||||
"lobby.chooseLeague": "لیگ را انتخاب کنید",
|
||||
"lobby.lvl": "سطح",
|
||||
"profile.uploadLocked": "آپلود عکس از سطح ۲۵ فعال میشود",
|
||||
"profile.uploadLocked": "آپلود عکس از سطح ۳ فعال میشود",
|
||||
|
||||
"forfeit.title": "تسلیم",
|
||||
"forfeit.ask": "از این بازی تسلیم میشوید؟",
|
||||
@@ -161,6 +161,13 @@ const fa: Dict = {
|
||||
"city.claimed": "جایزهٔ شهر دریافت شد ✓",
|
||||
"city.change": "تغییر",
|
||||
"city.unknown": "نامشخص",
|
||||
"report.button": "گزارش تخلف",
|
||||
"report.title": "علت گزارش این کاربر؟",
|
||||
"report.nudity": "تصویر نامناسب (مستهجن)",
|
||||
"report.insult": "توهین در گفتگو",
|
||||
"report.other": "موارد دیگر",
|
||||
"report.cancel": "انصراف",
|
||||
"report.done": "گزارش شما ثبت شد. ممنون!",
|
||||
"chat.empty": "گفتگو را شروع کنید",
|
||||
"friends.message": "پیام",
|
||||
|
||||
@@ -398,7 +405,7 @@ const en: Dict = {
|
||||
"achv.unlocksSticker": "Sticker",
|
||||
"lobby.chooseLeague": "Choose a league",
|
||||
"lobby.lvl": "Lvl",
|
||||
"profile.uploadLocked": "Photo upload unlocks at level 25",
|
||||
"profile.uploadLocked": "Photo upload unlocks at level 3",
|
||||
|
||||
"forfeit.title": "Forfeit",
|
||||
"forfeit.ask": "Surrender this match?",
|
||||
@@ -521,6 +528,13 @@ const en: Dict = {
|
||||
"city.claimed": "City reward claimed ✓",
|
||||
"city.change": "Change",
|
||||
"city.unknown": "Unknown",
|
||||
"report.button": "Report",
|
||||
"report.title": "Why are you reporting this player?",
|
||||
"report.nudity": "Inappropriate photo (nudity)",
|
||||
"report.insult": "Insulting chat",
|
||||
"report.other": "Something else",
|
||||
"report.cancel": "Cancel",
|
||||
"report.done": "Report submitted. Thank you!",
|
||||
"chat.empty": "Start the conversation",
|
||||
"friends.message": "Message",
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
PublicProfile,
|
||||
SocialLinks,
|
||||
SocialVisibility,
|
||||
ReportReason,
|
||||
RewardResult,
|
||||
Room,
|
||||
RoomSeat,
|
||||
@@ -589,6 +590,20 @@ export class MockOnlineService implements OnlineService {
|
||||
return () => this.friendCbs.delete(cb);
|
||||
}
|
||||
|
||||
async reportUser(targetId: string, reason: ReportReason, details?: string) {
|
||||
// Dev mock: just record it locally for moderation; the real backend persists.
|
||||
try {
|
||||
const key = "hokm.reports";
|
||||
const raw = typeof window !== "undefined" ? localStorage.getItem(key) : null;
|
||||
const list: unknown[] = raw ? JSON.parse(raw) : [];
|
||||
list.push({ targetId, reason, details: details ?? "", at: this.profile?.id ?? "me" });
|
||||
if (typeof window !== "undefined") localStorage.setItem(key, JSON.stringify(list));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/* ------------------------------- chat ------------------------------ */
|
||||
|
||||
private saveChats() {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
LeaderboardEntry,
|
||||
PlayerSummary,
|
||||
PublicProfile,
|
||||
ReportReason,
|
||||
MatchSummary,
|
||||
MatchmakingState,
|
||||
RewardResult,
|
||||
@@ -78,6 +79,8 @@ export interface OnlineService {
|
||||
declineRequest(id: string): Promise<void>;
|
||||
removeFriend(id: string): Promise<void>;
|
||||
onFriends(cb: (friends: Friend[]) => void): Unsubscribe;
|
||||
/** Report a player for an inappropriate avatar (nudity) or insulting chat. */
|
||||
reportUser(targetId: string, reason: ReportReason, details?: string): Promise<{ ok: boolean }>;
|
||||
|
||||
/* ----- chat ----- */
|
||||
listConversations(): Promise<Conversation[]>;
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
LeaderboardEntry,
|
||||
PlayerSummary,
|
||||
PublicProfile,
|
||||
ReportReason,
|
||||
MatchSummary,
|
||||
MatchmakingState,
|
||||
RewardResult,
|
||||
@@ -377,6 +378,10 @@ export class SignalrService implements OnlineService {
|
||||
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 }); }
|
||||
async reportUser(targetId: string, reason: ReportReason, details?: string) {
|
||||
await this.send<unknown>("POST", "/api/report", { targetId, reason, details });
|
||||
return { ok: true };
|
||||
}
|
||||
onFriends(cb: (f: Friend[]) => void) { this.friendCbs.add(cb); return () => this.friendCbs.delete(cb); }
|
||||
|
||||
createRoom(o: CreateRoomOptions) { return this.mock.createRoom(o); }
|
||||
|
||||
@@ -91,6 +91,9 @@ export interface UserProfile {
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
/** Why a player is being reported. */
|
||||
export type ReportReason = "nudity" | "insult" | "other";
|
||||
|
||||
/**
|
||||
* A public-facing view of another player — what you may see by tapping their
|
||||
* row in the leaderboard / friends list. No private fields (coins, phone,
|
||||
|
||||
Reference in New Issue
Block a user