From 857287fa844c0e92d08de3c09252866df999eea2 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Thu, 11 Jun 2026 19:32:49 +0330 Subject: [PATCH] mobile: fullscreen (immersive Android + PWA) + auto-hide reported nudity avatars Fullscreen on mobile: - Android (Capacitor): MainActivity now runs edge-to-edge and hides the status + navigation bars (immersive, transient-on-swipe), re-asserted on focus. - PWA: manifest display -> "fullscreen" with display_override fallback chain; viewport gains viewport-fit: cover for proper safe-area/edge-to-edge handling. Moderation auto-hide: - ProfileService.ReportUser now de-dupes nudity reports per reporter and, once NudityHideThreshold (3) distinct players flag a target's avatar as nudity, auto-removes their custom photo (reverts to default avatar). Counted from the ledger, so still no schema change. Co-Authored-By: Claude Opus 4.8 --- .../java/com/bargevasat/app/MainActivity.java | 35 +++++++++++++++++- public/manifest.webmanifest | 3 +- .../Hokm.Server/Profiles/ProfileService.cs | 37 ++++++++++++++++++- src/app/layout.tsx | 1 + 4 files changed, 72 insertions(+), 4 deletions(-) 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({