Preact: Add more user preferences in options menu (#2369)
Some checks are pending
Node.js CI / build (22.x) (push) Waiting to run

This commit is contained in:
Aurastic 2025-04-17 10:37:35 +05:30 committed by GitHub
parent 7c1ffc9e39
commit 0cc47f1f36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 407 additions and 17 deletions

View File

@ -154,11 +154,16 @@ export class BattleLog {
}
timestampHtml = `<small class="gray">[${components.map(x => x < 10 ? `0${x}` : x).join(':')}] </small>`;
}
let isHighlighted = window.app?.rooms?.[battle!.roomid].getHighlight(message);
let isHighlighted = window.app?.rooms?.[battle!.roomid].getHighlight(message) || window.PS?.getHighlight(message);
[divClass, divHTML, noNotify] = this.parseChatMessage(message, name, timestampHtml, isHighlighted);
if (!noNotify && isHighlighted) {
let notifyTitle = "Mentioned by " + name + " in " + battle!.roomid;
app.rooms[battle!.roomid].notifyOnce(notifyTitle, "\"" + message + "\"", 'highlight');
let notifyTitle = "Mentioned by " + name + " in " + (battle?.roomid || '');
window.app?.rooms[battle?.roomid || '']?.notifyOnce(notifyTitle, "\"" + message + "\"", 'highlight');
window.PS?.rooms[battle?.roomid || '']?.notify({
title: notifyTitle,
body: "\"" + message + "\"",
id: 'highlight',
});
}
break;
@ -234,7 +239,7 @@ export class BattleLog {
return;
case 'pm':
divHTML = '<strong>' + BattleLog.escapeHTML(args[1]) + ':</strong> <span class="message-pm"><i style="cursor:pointer" onclick="selectTab(\'lobby\');rooms.lobby.popupOpen(\'' + BattleLog.escapeHTML(args[2], true) + '\')">(Private to ' + BattleLog.escapeHTML(args[3]) + ')</i> ' + BattleLog.parseMessage(args[4]) + '</span>';
divHTML = `<strong data-href="user-${BattleLog.escapeHTML(args[1])}"> ${BattleLog.escapeHTML(args[1])}:</strong> <span class="message-pm"><i style="cursor:pointer" data-href="user-${BattleLog.escapeHTML(args[1], true)}">(Private to ${BattleLog.escapeHTML(args[2])})</i> ${BattleLog.parseMessage(args[3])} </span>`;
break;
case 'askreg':

View File

@ -55,6 +55,22 @@ class PSPrefs extends PSStreamModel<string | null> {
* null - Enable GIFs only on Chrome 64.
*/
nogif: boolean | null = null;
/* Graphics Preferences */
noanim: boolean | null = null;
bwgfx: boolean | null = null;
nopastgens: boolean | null = null;
/* Chat Preferences */
blockPMs: boolean | null = null;
blockChallenges: boolean | null = null;
inchatpm: boolean | null = null;
noselfhighlight: boolean | null = null;
temporarynotifications: boolean | null = null;
leavePopupRoom: boolean | null = null;
refreshprompt: boolean | null = null;
language: boolean | null = null;
/**
* Show "User joined" and "User left" messages. serverid:roomid
* table. Uses 1 and 0 instead of true/false for JSON packing
@ -366,6 +382,7 @@ class PSUser extends PSStreamModel<PSLoginState | null> {
loggingIn: string | null = null;
initializing = true;
gapiLoaded = false;
nameRegExp: RegExp | null = null;
setName(fullName: string, named: boolean, avatar: string) {
const loggingIn = (!this.named && named);
const { name, group } = BattleTextParser.parseNameParts(fullName);
@ -381,6 +398,7 @@ class PSUser extends PSStreamModel<PSLoginState | null> {
if (room.connectWhenLoggedIn) room.connect();
}
}
this.updateRegExp();
}
validateName(name: string): string {
// | , ; are not valid characters in names
@ -427,6 +445,7 @@ class PSUser extends PSStreamModel<PSLoginState | null> {
'getassertion', { userid, challstr: this.challstr }
).then(res => {
this.handleAssertion(name, res);
this.updateRegExp();
});
}
changeNameWithPassword(name: string, password: string, special: PSLoginState = { needsPassword: true }) {
@ -445,6 +464,7 @@ class PSUser extends PSStreamModel<PSLoginState | null> {
this.loggingIn = null;
if (data?.curuser?.loggedin) {
// success!
this.registered = true;
this.handleAssertion(name, data.assertion);
} else {
// wrong password
@ -513,6 +533,24 @@ class PSUser extends PSStreamModel<PSLoginState | null> {
this.registered = false;
this.update(null);
}
updateRegExp() {
if (!this.named) {
this.nameRegExp = null;
} else {
let escaped = this.name.replace(/[^A-Za-z0-9]+$/, '');
// we'll use `,` as a sentinel character to mean "any non-alphanumeric char"
// unicode characters can be replaced with any non-alphanumeric char
for (let i = escaped.length - 1; i > 0; i--) {
if (/[^ -~]/.test(escaped[i])) {
escaped = escaped.slice(0, i) + ',' + escaped.slice(i + 1);
}
}
escaped = escaped.replace(/[[\]/{}()*+?.\\^$|-]/g, "\\$&");
escaped = escaped.replace(/,/g, "[^A-Za-z0-9]?");
this.nameRegExp = new RegExp('(?:\\b|(?!\\w))' + escaped + '(?:\\b|\\B(?!\\w))', 'i');
}
}
}
/**********************************************************************
@ -1879,4 +1917,32 @@ export const PS = new class extends PSModel {
this.prefs.set('autojoin', autojoin);
}
}
getHighlight(message: string) {
if (!this.prefs.noselfhighlight && this.user.nameRegExp) {
if (this.user.nameRegExp.test(message)) return true;
}
/*
// TODO!
if (!this.highlightRegExp) {
try {
//this.updateHighlightRegExp(highlights);
} catch (e) {
// If the expression above is not a regexp, we'll get here.
// Don't throw an exception because that would prevent the chat
// message from showing up, or, when the lobby is initialising,
// it will prevent the initialisation from completing.
return false;
}
}
var id = PS.server.id + '#' + this.id;
var globalHighlightsRegExp = this.highlightRegExp['global'];
var roomHighlightsRegExp = this.highlightRegExp[id];
return (((globalHighlightsRegExp &&
globalHighlightsRegExp.test(message)) ||
(roomHighlightsRegExp && roomHighlightsRegExp.test(message))));
*/
return false;
}
};

View File

@ -36,6 +36,10 @@ export class BattlesRoom extends PSRoom {
constructor(options: RoomOptions) {
super(options);
this.refresh();
// If graphics preference is set to use BW sprites
if (PS.prefs.bwgfx) {
Dex.loadSpriteData('bw');
}
}
setFormat(format: string) {
if (format === this.format) return this.refresh();
@ -299,6 +303,7 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
break;
}
room.battle.add('|' + args.join('|'));
if (PS.prefs.noanim) this.props.room.battle.seekTurn(Infinity);
}
receiveRequest(request: BattleRequest | null) {
const room = this.props.room;

View File

@ -124,6 +124,8 @@ export class MainMenuRoom extends PSRoom {
} case 'pm': {
const [, user1, user2, message] = args;
this.handlePM(user1, user2, message);
let sideRoom = PS.rightPanel as ChatRoom;
if (sideRoom?.type === "chat" && PS.prefs.inchatpm) sideRoom?.log?.add(args);
return;
} case 'formats': {
this.parseFormats(args);

View File

@ -2,6 +2,7 @@ import preact from "../js/lib/preact";
import { toID, toRoomid, toUserid, Dex } from "./battle-dex";
import type { ID } from "./battle-dex-data";
import { BattleLog } from "./battle-log";
import { PSLoginServer } from "./client-connection";
import { PSRoom, type RoomOptions, PS, type PSLoginState, type RoomID, type TimestampOptions } from "./client-main";
import { type BattleRoom } from "./panel-battle";
import { PSRoomPanel, PSPanelWrapper } from "./panels";
@ -303,12 +304,12 @@ class OptionsPanel extends PSRoomPanel {
}
PS.update();
};
setChatroomTimestamp = (e: Event) => {
const timestamp = (e.currentTarget as HTMLSelectElement).value as TimestampOptions;
setChatroomTimestamp = (ev: Event) => {
const timestamp = (ev.currentTarget as HTMLSelectElement).value as TimestampOptions;
PS.prefs.set('timestamps', { ...PS.prefs.timestamps, chatrooms: timestamp || undefined });
};
setPMsTimestamp = (e: Event) => {
const timestamp = (e.currentTarget as HTMLSelectElement).value as TimestampOptions;
setPMsTimestamp = (ev: Event) => {
const timestamp = (ev.currentTarget as HTMLSelectElement).value as TimestampOptions;
PS.prefs.set('timestamps', { ...PS.prefs.timestamps, pms: timestamp || undefined });
};
@ -318,6 +319,34 @@ class OptionsPanel extends PSRoomPanel {
this.setState({ showStatusInput: !this.state.showStatusInput });
};
handleOnChange = (ev: Event) => {
let elem = ev.currentTarget as HTMLInputElement;
let setting = elem.name;
let value = elem.checked;
switch (setting) {
case 'blockPMs': {
PS.prefs.set("blockPMs", value);
PS.send(value ? '/blockpms' : '/unblockpms');
break;
}
case 'blockChallenges': {
PS.prefs.set("blockChallenges", value);
PS.send(value ? '/blockchallenges' : '/unblockchallenges');
break;
}
case 'bwgfx': {
PS.prefs.set('bwgfx', value);
Dex.loadSpriteData(value || PS.prefs.noanim ? 'bw' : 'xy');
break;
}
case 'noanim':
case 'nopastgens':
case 'noselfhighlight':
case 'inchatpm': PS.prefs.set(setting, value);
break;
}
};
editStatus = (ev: Event) => {
const statusInput = this.base!.querySelector<HTMLInputElement>('input[name=statustext]');
PS.send(statusInput?.value?.length ? `|/status ${statusInput.value}` : `|/clearstatus`);
@ -342,7 +371,7 @@ class OptionsPanel extends PSRoomPanel {
{this.state.showStatusInput ? (
<p>
<input name="statustext"></input>
<input name="statustext" />
<button class="button" onClick={this.editStatus}><i class="fa fa-pencil"></i></button>
</p>
) : (
@ -352,6 +381,10 @@ class OptionsPanel extends PSRoomPanel {
</p>
)}
{PS.user.named && (PS.user.registered ?
<button className="button" data-href="changepassword">Change Password</button> :
<button className="button" data-href="register">Register</button>)}
<hr />
<h3>Graphics</h3>
<p>
@ -368,15 +401,67 @@ class OptionsPanel extends PSRoomPanel {
<option value="vertical" selected={PS.prefs.onepanel === 'vertical'}>Vertical tabs</option>
</select></label>
</p>
<hr />
{PS.user.named ? <p class="buttonbar" style="text-align: right">
<button class="button" data-href="login"><i class="fa fa-pencil"></i> Change name</button> {}
<button class="button" data-cmd="/logout"><i class="fa fa-power-off"></i> Log out</button>
</p> : <p class="buttonbar" style="text-align: right">
<button class="button" data-href="login"><i class="fa fa-pencil"></i> Choose name</button>
</p> }
<p>
<label class="checkbox">
<input
type="checkbox" name="noanim"
checked={PS.prefs.noanim || false} onChange={this.handleOnChange}
/> Disable animations
</label>
</p>
<p>
<label class="checkbox">
<input
type="checkbox"
name="bwgfx"
onChange={this.handleOnChange}
checked={PS.prefs.bwgfx || false}
/> Use 2D sprites instead of 3D models</label>
</p>
<p>
<label class="checkbox"><input
type="checkbox"
name="nopastgens"
onChange={this.handleOnChange}
checked={PS.prefs.nopastgens || false}
/> Use modern sprites for past generations</label>
</p>
<hr />
<h3>Chat</h3>
<p>
<label class="checkbox">
<input type="checkbox" onChange={this.handleOnChange} name="blockPMs" checked={PS.prefs.blockPMs || false}>
</input> Block PMs
</label>
</p>
<p>
<label class="checkbox">
<input
type="checkbox"
name="blockChallenges"
onChange={this.handleOnChange}
checked={PS.prefs.blockChallenges || false}
>
</input> Block Challenges</label>
</p>
<p>
<label class="checkbox">
<input
type="checkbox"
name="inchatpm"
onChange={this.handleOnChange}
checked={PS.prefs.inchatpm || false}
/> Show PMs in chatrooms</label>
</p>
<p>
<label class="checkbox">
<input
type="checkbox"
name="noselfhighlight"
onChange={this.handleOnChange}
checked={PS.prefs.noselfhighlight || false}
/> Do Not Highlight when your name is said in chat</label>
</p>
<p>
<label class="optlabel">Timestamps: <select name="layout" class="button" onChange={this.setChatroomTimestamp}>
<option value="" selected={!PS.prefs.timestamps.chatrooms}>Off</option>
@ -391,6 +476,13 @@ class OptionsPanel extends PSRoomPanel {
<option value="seconds" selected={PS.prefs.timestamps.pms === "seconds"}>[HH:MM:SS]</option>
</select></label>
</p>
<hr />
{PS.user.named ? <p class="buttonbar" style="text-align: right">
<button class="button" data-href="login"><i class="fa fa-pencil"></i> Change name</button> {}
<button class="button" data-cmd="/logout"><i class="fa fa-power-off"></i> Log out</button>
</p> : <p class="buttonbar" style="text-align: right">
<button class="button" data-href="login"><i class="fa fa-pencil"></i> Choose name</button>
</p> }
</div></PSPanelWrapper>;
}
}
@ -642,7 +734,7 @@ class BattleForfeitPanel extends PSRoomPanel {
<form>
<p>Forfeiting makes you lose the battle. Are you sure?</p>
<p>
<label class="checkbox"><input type="checkbox" name="closeroom" checked={true}></input> Close after
<label class="checkbox"><input type="checkbox" name="closeroom" checked={true} /> Close after
forfeiting</label>
</p>
<p>
@ -703,6 +795,224 @@ class ReplacePlayerPanel extends PSRoomPanel {
}
}
class ChangePasswordPanel extends PSRoomPanel {
static readonly id = "changepassword";
static readonly routes = ["changepassword"];
static readonly location = "semimodal-popup";
static readonly noURL = true;
declare state: { errorMsg: string };
update = () => {
this.forceUpdate();
};
handleChangePassword = (ev: Event) => {
ev.preventDefault();
let oldpassword = this.base?.querySelector<HTMLInputElement>('input[name=oldpassword]')?.value;
let password = this.base?.querySelector<HTMLInputElement>('input[name=password]')?.value;
let cpassword = this.base?.querySelector<HTMLInputElement>('input[name=cpassword]')?.value;
if (!oldpassword?.length ||
!password?.length ||
!cpassword?.length) return this.setState({ errorMsg: "All fields are required" });
if (password !== cpassword) return this.setState({ errorMsg: 'Passwords do not match' });
PSLoginServer.query("changepassword", {
oldpassword,
password,
cpassword,
}).then(data => {
if (data?.actionerror) return this.setState({ errorMsg: data?.actionerror });
PS.alert("Your password was successfully changed!");
}).catch(err => {
console.error(err);
this.setState({ errorMsg: err.message });
});
this.setState({ errorMsg: '' });
};
override render() {
const room = this.props.room;
return <PSPanelWrapper room={room} width={280}><div class="pad">
<form onSubmit={this.handleChangePassword}>
{ !!this.state.errorMsg?.length && <p>
<b class="message-error"> {this.state.errorMsg}</b>
</p> }
<p>Change your password:</p>
<p>
<label class="label">Username:
<strong><input
type="text"
name="username"
value={PS.user.name}
style="
color: inherit;
background: transparent;
border: 0;
font: inherit;
font-size: inherit;
display: block;
"
readOnly={true}
autocomplete="username"
/></strong></label>
</p>
<p>
<label class="label">Old password:
<input
class="textbox autofocus"
type="password"
name="oldpassword"
autocomplete="current-password"
/></label>
</p>
<p>
<label class="label">New password:
<input
class="textbox"
type="password"
name="password"
autocomplete="new-password"
/></label>
</p>
<p>
<label class="label">New password (confirm):
<input
class="textbox"
type="password"
name="cpassword"
autocomplete="new-password"
/></label>
</p>
<p class="buttonbar">
<button type="submit" class="button">
<strong>Change password</strong>
</button>
<button type="button" data-cmd="/close" class="button">Cancel</button>
</p>
</form>
</div>
</PSPanelWrapper>;
}
}
class RegisterPanel extends PSRoomPanel {
static readonly id = "register";
static readonly routes = ["register"];
static readonly location = "semimodal-popup";
static readonly noURL = true;
static readonly rightPopup = true;
declare state: { errorMsg: string };
update = () => {
this.forceUpdate();
};
handleRegisterUser = (ev: Event) => {
ev.preventDefault();
let captcha = this.base?.querySelector<HTMLInputElement>('input[name=captcha]')?.value;
let password = this.base?.querySelector<HTMLInputElement>('input[name=password]')?.value;
let cpassword = this.base?.querySelector<HTMLInputElement>('input[name=cpassword]')?.value;
if (!captcha?.length ||
!password?.length ||
!cpassword?.length) return this.setState({ errorMsg: "All fields are required" });
if (password !== cpassword) return this.setState({ errorMsg: 'Passwords do not match' });
PSLoginServer.query("register", {
captcha,
password,
cpassword,
username: PS.user.name,
challstr: PS.user.challstr,
}).then(data => {
console.log(data);
if (data?.actionerror) this.setState({ errorMsg: data?.actionerror });
if (data?.curuser?.loggedin) {
PS.user.registered = true;
let name = data.curuser.username;
if (data?.assertion) PS.user.handleAssertion(name, data?.assertion);
this.close();
PS.alert("You have been successfully registered.");
}
}).catch(err => {
console.error(err);
this.setState({ errorMsg: err.message });
});
this.setState({ errorMsg: '' });
};
override render() {
const room = this.props.room;
return <PSPanelWrapper room={room} width={280}><div class="pad">
<form onSubmit={this.handleRegisterUser}>
{ !!this.state.errorMsg?.length && <p>
<b class="message-error"> {this.state.errorMsg}</b>
</p> }
<p>Register your account:</p>
<p>
<label class="label">Username:
<strong><input
type="text"
name="name"
value={PS.user.name}
style="
color: inherit;
background: transparent;
border: 0;
font: inherit;
font-size: inherit;
display: block;
"
readOnly={true}
autocomplete="username"
/></strong></label>
</p>
<p>
<label class="label">Password:
<input
class="textbox autofocus"
type="password"
name="password"
autocomplete="new-password"
/></label>
</p>
<p>
<label class="label">Password (confirm):
<input
class="textbox"
type="password"
name="cpassword"
autocomplete="new-password"
/></label>
</p>
<p>
<label class="label"> <img
src="https://play.pokemonshowdown.com/sprites/gen5ani/pikachu.gif"
alt="An Electric-type mouse that is the mascot of the Pokémon franchise."
/></label>
</p>
<p>
<label class="label">What is this pokemon?
<input
class="textbox" type="text" name="captcha" value=""
/></label>
</p>
<p class="buttonbar">
<button type="submit" class="button"><strong>Register</strong></button>
<button type="button" data-cmd="/close" class="button">Cancel</button>
</p>
</form>
</div>
</PSPanelWrapper>;
}
}
class PopupPanel extends PSRoomPanel {
static readonly id = 'popup';
static readonly routes = ['popup-*'];
@ -729,6 +1039,8 @@ PS.addRoomType(UserPanel,
OptionsPanel,
LoginPanel,
AvatarsPanel,
ChangePasswordPanel,
RegisterPanel,
BattleForfeitPanel,
ReplacePlayerPanel,
PopupPanel);