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;
|
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;
|
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",
|
"lang": "fa",
|
||||||
"dir": "rtl",
|
"dir": "rtl",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"display": "standalone",
|
"display": "fullscreen",
|
||||||
|
"display_override": ["fullscreen", "standalone", "minimal-ui"],
|
||||||
"orientation": "any",
|
"orientation": "any",
|
||||||
"background_color": "#060c1f",
|
"background_color": "#060c1f",
|
||||||
"theme_color": "#060c1f",
|
"theme_color": "#060c1f",
|
||||||
|
|||||||
@@ -54,19 +54,52 @@ public class ProfileService
|
|||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Distinct players who must flag an avatar as nudity before it auto-hides.</summary>
|
||||||
|
public const int NudityHideThreshold = 3;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Record a moderation report (inappropriate avatar / insulting chat). Stored
|
/// Record a moderation report (inappropriate avatar / insulting chat). Stored
|
||||||
/// in the write-only ledger as kind="report" so no schema change is needed;
|
/// 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>
|
/// </summary>
|
||||||
public async Task ReportUser(string reporterUid, string targetId, string? reason, string? details)
|
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 safeReason = reason is "nudity" or "insult" or "other" ? reason : "other";
|
||||||
var safeDetails = (details ?? "").Replace("\n", " ").Trim();
|
var safeDetails = (details ?? "").Replace("\n", " ").Trim();
|
||||||
var @ref = $"{targetId}|{safeReason}|{safeDetails}";
|
var @ref = $"{targetId}|{safeReason}|{safeDetails}";
|
||||||
if (@ref.Length > 480) @ref = @ref[..480];
|
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);
|
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)
|
public async Task<ProfileDto> Update(string uid, JsonElement patch)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const viewport: Viewport = {
|
|||||||
initialScale: 1,
|
initialScale: 1,
|
||||||
maximumScale: 1,
|
maximumScale: 1,
|
||||||
userScalable: false,
|
userScalable: false,
|
||||||
|
viewportFit: "cover",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|||||||
Reference in New Issue
Block a user