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 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -18,6 +18,7 @@ export const viewport: Viewport = {
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
userScalable: false,
|
||||
viewportFit: "cover",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
Reference in New Issue
Block a user