mobile: fullscreen (immersive Android + PWA) + auto-hide reported nudity avatars
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 23s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m11s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m3s

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 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-11 19:32:49 +03:30
parent 6641741669
commit 857287fa84
4 changed files with 72 additions and 4 deletions
@@ -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);
}
}
}
+2 -1
View File
@@ -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",
@@ -54,19 +54,52 @@ public class ProfileService
await _db.SaveChangesAsync();
}
/// <summary>Distinct players who must flag an avatar as nudity before it auto-hides.</summary>
public const int NudityHideThreshold = 3;
/// <summary>
/// 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.
/// </summary>
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<ProfileDto> Update(string uid, JsonElement patch)
+1
View File
@@ -18,6 +18,7 @@ export const viewport: Viewport = {
initialScale: 1,
maximumScale: 1,
userScalable: false,
viewportFit: "cover",
};
export default function RootLayout({