Preact: Support desktop notifications (#2441)

---------

Co-authored-by: Guangcong Luo <guangcongluo@gmail.com>
This commit is contained in:
Aurastic 2025-05-28 14:14:01 +05:30 committed by GitHub
parent 2a195dac3b
commit b1fc3b2d9e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 106 additions and 5 deletions

View File

@ -1304,7 +1304,7 @@ export class BattleLog {
this.changeUhtml(parts[0], htmlSrc, cmd === 'uhtml');
return ['', ''];
case 'raw':
return ['chat', BattleLog.sanitizeHTML(target)];
return ['chat', BattleLog.sanitizeHTML(target), true];
case 'nonotify':
return ['chat', BattleLog.sanitizeHTML(target), true];
default:

View File

@ -899,6 +899,7 @@ interface PSNotificationState {
id: string;
/** normally: automatically dismiss the notification when viewing the room; set this to require manual dismissing */
noAutoDismiss: boolean;
notification?: Notification | null;
}
type ClientCommands<RoomT extends PSRoom> = {
@ -1003,7 +1004,24 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
return null;
}
notify(options: { title: string, body?: string, noAutoDismiss?: boolean, id?: string }) {
if (PS.isVisible(this)) return;
let desktopNotification: Notification | null = null;
const roomIsFocused = document.hasFocus?.() && PS.isVisible(this);
if (roomIsFocused && !options.noAutoDismiss) return;
if (!roomIsFocused) {
PS.playNotificationSound();
try {
desktopNotification = new Notification(options.title, { body: options.body });
if (desktopNotification) {
desktopNotification.onclick = () => {
window.focus();
PS.focusRoom(this.id);
};
if (PS.prefs.temporarynotifications) {
setTimeout(() => { desktopNotification?.close(); }, 5000);
}
}
} catch {}
}
if (options.noAutoDismiss && !options.id) {
throw new Error(`Must specify id for manual dismissing`);
}
@ -1015,6 +1033,7 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
body: options.body,
id: options.id || '',
noAutoDismiss: options.noAutoDismiss || false,
notification: desktopNotification,
});
PS.update();
}
@ -1024,7 +1043,13 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
PS.update();
}
dismissNotification(id: string) {
this.notifications = this.notifications.filter(notification => notification.id !== id);
const index = this.notifications.findIndex(n => n.id === id);
if (index !== -1) {
try {
this.notifications[index].notification?.close();
} catch {}
this.notifications.splice(index, 1);
}
PS.update();
}
autoDismissNotifications() {
@ -1866,6 +1891,11 @@ export const PS = new class extends PSModel {
}
}
// for old versions of Safari
if (window.webkitNotification) {
window.Notification ||= window.webkitNotification;
}
this.updateLayout();
window.addEventListener('resize', () => {
// super.update() skips another updateLayout() call
@ -2331,10 +2361,12 @@ export const PS = new class extends PSModel {
addRoom(options: RoomOptions, noFocus = false) {
// support hardcoded PM room-IDs
if (options.id.startsWith('challenge-')) {
this.requestNotifications();
options.id = `dm-${options.id.slice(10)}` as RoomID;
options.args = { challengeMenuOpen: true };
}
if (options.id.startsWith('dm-')) {
this.requestNotifications();
if (options.id.length >= 5 && options.id.endsWith('--')) {
options.id = options.id.slice(0, -2) as RoomID;
options.args = { initialSlash: true };
@ -2628,4 +2660,23 @@ export const PS = new class extends PSModel {
this.prefs.set('autojoin', autojoin);
}
}
requestNotifications() {
try {
if (window.webkitNotifications?.requestPermission) {
// Notification.requestPermission crashes Chrome 23:
// https://code.google.com/p/chromium/issues/detail?id=139594
// In lieu of a way to detect Chrome 23, we'll just use the old
// requestPermission API, which works to request permissions for
// the new Notification spec anyway.
window.webkitNotifications.requestPermission();
} else if (window.Notification) {
Notification.requestPermission?.(permission => {});
}
} catch {}
}
playNotificationSound() {
if (window.BattleSound && !this.prefs.mute) {
window.BattleSound.playSound('audio/notification.wav', this.prefs.notifvolume);
}
}
};

View File

@ -366,8 +366,25 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
room.request = request;
room.choices = new BattleChoiceBuilder(request);
this.notifyRequest();
room.update(null);
}
notifyRequest() {
const room = this.props.room;
let oName = room.battle.farSide.name;
if (oName) oName = " against " + oName;
switch (room.request?.requestType) {
case 'move':
room.notify({ title: "Your move!", body: "Move in your battle" + oName });
break;
case 'switch':
room.notify({ title: "Your switch!", body: "Switch in your battle" + oName });
break;
case 'team':
room.notify({ title: "Team preview!", body: "Choose your team order in your battle" + oName });
break;
}
}
renderControls() {
const room = this.props.room;
if (!room.battle) return null;

View File

@ -125,7 +125,17 @@ export class ChatRoom extends PSRoom {
this.joinLeave = null;
this.markUserActive(args[args[0] === 'c:' ? 2 : 1]);
if (this.tour) this.tour.joinLeave = null;
this.subtleNotify();
if (this.id.startsWith("dm-")) {
const fromUser = args[args[0] === 'c:' ? 2 : 1];
if (toID(fromUser) === PS.user.userid) break;
const message = args[args[0] === 'c:' ? 3 : 2];
this.notify({
title: `${this.title}`,
body: message,
});
} else {
this.subtleNotify();
}
break;
case ':':
this.timeOffset = Math.trunc(Date.now() / 1000) - (parseInt(args[1], 10) || 0);
@ -1110,6 +1120,7 @@ class ChatPanel extends PSRoomPanel<ChatRoom> {
return false;
};
makeChallenge = (e: Event, format: string, team?: Team) => {
PS.requestNotifications();
const room = this.props.room;
const packedTeam = team ? team.packedTeam : '';
const privacy = PS.mainmenu.adjustPrivacy();

View File

@ -65,6 +65,7 @@ export class MainMenuRoom extends PSRoom {
return '';
}
startSearch = (format: string, team?: Team) => {
PS.requestNotifications();
if (this.searchCountdown) {
PS.alert("Wait for this countdown to finish first...");
return;

View File

@ -324,6 +324,11 @@ export class PSMiniHeader extends preact.Component {
override render() {
if (PS.leftPanelWidth !== null) return null;
let notificationsCount = 0;
for (const roomid of PS.leftRoomList) {
const miniNotifications = PS.rooms[roomid]?.notifications;
if (miniNotifications?.length) notificationsCount++;
}
const { icon, title } = PSHeader.roomInfo(PS.panel);
const userColor = window.BattleLog && `color:${BattleLog.usernameColor(PS.user.userid)}`;
const showMenuButton = PSView.narrowMode;
@ -334,6 +339,7 @@ export class PSMiniHeader extends preact.Component {
null
) : window.scrollX ? (
<button onClick={PSView.scrollToHeader} class={`mini-header-left ${notifying}`} aria-label="Menu">
{!!notificationsCount && <div class="notification-badge">{notificationsCount}</div>}
<i class="fa fa-bars" aria-hidden></i>
</button>
) : (

View File

@ -1494,7 +1494,7 @@ pre.textbox.textbox-empty[placeholder]:before {
}
.userlist-hidden {
width: auto;
border-right: 0;
border-right: 0;
bottom: auto;
overflow: visible;
}
@ -2451,3 +2451,18 @@ pre.textbox.textbox-empty[placeholder]:before {
.dark .chat.mine {
background: rgba(255,255,255,0.05);
}
.notification-badge {
position: absolute;
top: 2px;
left: 13px;
width: 15px;
height: 15px;
background: #e9790d;
color: white;
font-size: 9px;
font-weight: bold;
line-height: 14px;
text-align: center;
border-radius: 50%;
}