mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-03 21:50:45 +00:00
1049b7e7dc
## Description: Properly split out clantags and usernames, a clantag should not be part of a username. <img width="285" height="286" alt="image" src="https://github.com/user-attachments/assets/8ac56e82-b12c-4fc0-9774-e445252a6e61" /> https://api.openfront.dev/game/ojkqZFb2 <img width="296" height="596" alt="image" src="https://github.com/user-attachments/assets/85152f80-c111-4f87-b85b-8516c9c6137b" /> https://api.openfront.dev/game/MF32BkVc requires; https://github.com/openfrontio/infra/pull/264 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n
168 lines
5.3 KiB
TypeScript
168 lines
5.3 KiB
TypeScript
import { html, LitElement } from "lit";
|
|
import { customElement, property } from "lit/decorators.js";
|
|
import { EventBus } from "../../../core/EventBus";
|
|
import { PlayerType } from "../../../core/game/Game";
|
|
import { PlayerView } from "../../../core/game/GameView";
|
|
import { actionButton } from "../../components/ui/ActionButton";
|
|
import { SendKickPlayerIntentEvent } from "../../Transport";
|
|
import { translateText } from "../../Utils";
|
|
import kickIcon from "/images/ExitIconWhite.svg?url";
|
|
import shieldIcon from "/images/ShieldIconWhite.svg?url";
|
|
|
|
@customElement("player-moderation-modal")
|
|
export class PlayerModerationModal extends LitElement {
|
|
@property({ attribute: false }) eventBus: EventBus | null = null;
|
|
@property({ attribute: false }) myPlayer: PlayerView | null = null;
|
|
@property({ attribute: false }) target: PlayerView | null = null;
|
|
|
|
@property({ type: Boolean }) open: boolean = false;
|
|
@property({ type: Boolean }) alreadyKicked: boolean = false;
|
|
|
|
createRenderRoot() {
|
|
return this;
|
|
}
|
|
|
|
updated(changed: Map<string, unknown>) {
|
|
if (changed.has("open") && this.open) {
|
|
queueMicrotask(() =>
|
|
(this.querySelector('[role="dialog"]') as HTMLElement | null)?.focus(),
|
|
);
|
|
}
|
|
}
|
|
|
|
private closeModal() {
|
|
this.dispatchEvent(new CustomEvent("close"));
|
|
}
|
|
|
|
private handleKeydown = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") {
|
|
e.preventDefault();
|
|
this.closeModal();
|
|
}
|
|
};
|
|
|
|
private canKick(my: PlayerView, other: PlayerView): boolean {
|
|
return (
|
|
my.isLobbyCreator() &&
|
|
other !== my &&
|
|
other.type() === PlayerType.Human &&
|
|
!!other.clientID()
|
|
);
|
|
}
|
|
|
|
private handleKickClick = (e: MouseEvent) => {
|
|
e.stopPropagation();
|
|
|
|
const my = this.myPlayer;
|
|
const other = this.target;
|
|
const eventBus = this.eventBus;
|
|
|
|
if (!my || !other) return;
|
|
if (!this.canKick(my, other) || this.alreadyKicked) return;
|
|
if (!eventBus) return;
|
|
|
|
const targetClientID = other.clientID();
|
|
if (!targetClientID || targetClientID.length === 0) return;
|
|
|
|
const confirmed = confirm(
|
|
translateText("player_panel.kick_confirm", { name: other.displayName() }),
|
|
);
|
|
if (!confirmed) return;
|
|
|
|
eventBus.emit(new SendKickPlayerIntentEvent(targetClientID));
|
|
this.dispatchEvent(
|
|
new CustomEvent("kicked", { detail: { playerId: String(other.id()) } }),
|
|
);
|
|
this.closeModal();
|
|
};
|
|
|
|
render() {
|
|
if (!this.open) return html``;
|
|
|
|
const my = this.myPlayer;
|
|
const other = this.target;
|
|
if (!my || !other) return html``;
|
|
|
|
const canKick = this.canKick(my, other);
|
|
const alreadyKicked = this.alreadyKicked;
|
|
|
|
const moderationTitle = translateText("player_panel.moderation");
|
|
const kickTitle = alreadyKicked
|
|
? translateText("player_panel.kicked")
|
|
: translateText("player_panel.kick");
|
|
|
|
return html`
|
|
<div class="absolute inset-0 z-1200 flex items-center justify-center p-4">
|
|
<div
|
|
class="absolute inset-0 bg-black/60 rounded-2xl"
|
|
@click=${() => this.closeModal()}
|
|
></div>
|
|
|
|
<div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="moderation-title"
|
|
class="relative z-10 w-full max-w-120 focus:outline-hidden"
|
|
tabindex="0"
|
|
@keydown=${this.handleKeydown}
|
|
>
|
|
<div
|
|
class="rounded-2xl bg-zinc-900 p-5 shadow-2xl ring-1 ring-zinc-800 max-h-[90vh] text-zinc-200"
|
|
@click=${(e: MouseEvent) => e.stopPropagation()}
|
|
>
|
|
<div class="mb-3 flex items-center justify-between relative">
|
|
<div class="flex items-center gap-2">
|
|
<img
|
|
src=${shieldIcon}
|
|
alt=""
|
|
aria-hidden="true"
|
|
class="h-5 w-5"
|
|
/>
|
|
<h2
|
|
id="moderation-title"
|
|
class="text-lg font-semibold tracking-tight text-zinc-100"
|
|
>
|
|
${moderationTitle}
|
|
</h2>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
@click=${() => this.closeModal()}
|
|
class="absolute -top-3 -right-3 flex h-7 w-7 items-center justify-center rounded-full bg-zinc-700 text-white shadow-sm hover:bg-red-500 transition-colors focus-visible:ring-2 focus-visible:ring-white/30 focus:outline-hidden"
|
|
aria-label=${translateText("common.close")}
|
|
title=${translateText("common.close")}
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
<div
|
|
class="mb-4 rounded-xl border border-white/10 bg-white/5 px-3 py-2"
|
|
>
|
|
<div
|
|
class="text-sm font-semibold text-zinc-100 truncate"
|
|
title=${other.displayName()}
|
|
>
|
|
${other.displayName()}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid auto-cols-fr grid-flow-col gap-1">
|
|
${actionButton({
|
|
onClick: this.handleKickClick,
|
|
icon: kickIcon,
|
|
iconAlt: "Kick",
|
|
title: kickTitle,
|
|
label: kickTitle,
|
|
type: "red",
|
|
disabled: alreadyKicked || !canKick,
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|