diff --git a/android/app/src/main/java/com/bargevasat/app/MainActivity.java b/android/app/src/main/java/com/bargevasat/app/MainActivity.java
index ffb7ee8..e84ad3b 100644
--- a/android/app/src/main/java/com/bargevasat/app/MainActivity.java
+++ b/android/app/src/main/java/com/bargevasat/app/MainActivity.java
@@ -1,5 +1,38 @@
package com.bargevasat.app;
+import android.os.Bundle;
+import androidx.core.view.WindowCompat;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.core.view.WindowInsetsControllerCompat;
import com.getcapacitor.BridgeActivity;
-public class MainActivity extends BridgeActivity {}
+/**
+ * Runs the game edge-to-edge and hides the Android status + navigation bars for
+ * a fullscreen, console-like experience (they reappear transiently on a swipe).
+ */
+public class MainActivity extends BridgeActivity {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ enableImmersive();
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ // Re-assert immersive mode whenever the window regains focus (e.g. after a
+ // system dialog, or the bars were swiped in).
+ if (hasFocus) enableImmersive();
+ }
+
+ private void enableImmersive() {
+ WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
+ WindowInsetsControllerCompat controller =
+ WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
+ if (controller != null) {
+ controller.hide(WindowInsetsCompat.Type.systemBars());
+ controller.setSystemBarsBehavior(
+ WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
+ }
+ }
+}
diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest
index 84d09fb..f46954e 100644
--- a/public/manifest.webmanifest
+++ b/public/manifest.webmanifest
@@ -5,7 +5,8 @@
"lang": "fa",
"dir": "rtl",
"start_url": "/",
- "display": "standalone",
+ "display": "fullscreen",
+ "display_override": ["fullscreen", "standalone", "minimal-ui"],
"orientation": "any",
"background_color": "#060c1f",
"theme_color": "#060c1f",
diff --git a/server/src/Hokm.Server/Profiles/ProfileService.cs b/server/src/Hokm.Server/Profiles/ProfileService.cs
index c373be9..c458c24 100644
--- a/server/src/Hokm.Server/Profiles/ProfileService.cs
+++ b/server/src/Hokm.Server/Profiles/ProfileService.cs
@@ -54,19 +54,52 @@ public class ProfileService
await _db.SaveChangesAsync();
}
+ /// Distinct players who must flag an avatar as nudity before it auto-hides.
+ public const int NudityHideThreshold = 3;
+
///
/// Record a moderation report (inappropriate avatar / insulting chat). Stored
/// in the write-only ledger as kind="report" so no schema change is needed;
- /// Ref encodes "{targetId}|{reason}|{details}".
+ /// Ref encodes "{targetId}|{reason}|{details}". Once enough *distinct* players
+ /// flag a target's avatar as nudity, the custom photo is auto-removed.
///
public async Task ReportUser(string reporterUid, string targetId, string? reason, string? details)
{
- if (string.IsNullOrWhiteSpace(targetId)) return;
+ if (string.IsNullOrWhiteSpace(targetId) || targetId == reporterUid) return;
var safeReason = reason is "nudity" or "insult" or "other" ? reason : "other";
var safeDetails = (details ?? "").Replace("\n", " ").Trim();
var @ref = $"{targetId}|{safeReason}|{safeDetails}";
if (@ref.Length > 480) @ref = @ref[..480];
+
+ var nudityPrefix = targetId + "|nudity";
+ // De-dupe nudity reports so a single player can't nuke an avatar alone.
+ if (safeReason == "nudity")
+ {
+ var already = await _db.Ledger.AnyAsync(
+ l => l.Kind == "report" && l.UserId == reporterUid && l.Ref!.StartsWith(nudityPrefix));
+ if (already) return;
+ }
+
await Ledger(reporterUid, "report", 0, @ref);
+
+ // Auto-hide a custom avatar once enough distinct players flag it.
+ if (safeReason == "nudity")
+ {
+ var reporters = await _db.Ledger
+ .Where(l => l.Kind == "report" && l.Ref!.StartsWith(nudityPrefix))
+ .Select(l => l.UserId)
+ .Distinct()
+ .CountAsync();
+ if (reporters >= NudityHideThreshold)
+ {
+ var target = await GetOrCreate(targetId, null);
+ if (!string.IsNullOrEmpty(target.AvatarImage))
+ {
+ target.AvatarImage = null; // revert to their default avatar
+ await SaveInternal(target);
+ }
+ }
+ }
}
public async Task Update(string uid, JsonElement patch)
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 9b8d42d..527b111 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -18,6 +18,7 @@ export const viewport: Viewport = {
initialScale: 1,
maximumScale: 1,
userScalable: false,
+ viewportFit: "cover",
};
export default function RootLayout({