Merge pull request #3 from SKATTKOOKIE/feature/add-sound-for-role-notification

Feature/add sound for role notification
This commit is contained in:
Brandon 2026-02-28 12:27:02 +00:00 committed by GitHub
commit 8f6d502b3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 171 additions and 7 deletions

View File

@ -1,3 +1,5 @@
import { toastScript } from "./toasts";
export function injectSounds(webContents: Electron.WebContents) {
webContents.on("did-finish-load", () => {
webContents.executeJavaScript(`
@ -5,9 +7,13 @@ export function injectSounds(webContents: Electron.WebContents) {
if (window.__soundsInjected) return;
window.__soundsInjected = true;
${toastScript}
let ctx = null;
let currentUserId = null;
let currentChannelId = null;
const roleNameCache = {};
const userNameCache = {};
function getAudioContext() {
if (!ctx) ctx = new AudioContext();
@ -41,13 +47,16 @@ export function injectSounds(webContents: Electron.WebContents) {
}
function playUserJoinSound() {
playTone(880, 0.15);
setTimeout(() => playTone(1100, 0.2), 150);
playTone(880, 0.15, 0.05);
}
function playUserLeaveSound() {
playTone(440, 0.15);
setTimeout(() => playTone(220, 0.3), 150);
playTone(440, 0.15, 0.05);
}
function playMentionSound() {
playTone(1200, 0.1, 0.08);
setTimeout(() => playTone(1200, 0.15, 0.08), 120);
}
const OriginalWebSocket = window.WebSocket;
@ -60,9 +69,27 @@ export function injectSounds(webContents: Electron.WebContents) {
try {
const data = JSON.parse(event.data);
if (data.type === "Ready" && data.users) {
const self = data.users.find(u => u.relationship === "User");
if (self) currentUserId = self._id;
if (data.type === "Ready") {
if (data.users) {
const self = data.users.find(u => u.relationship === "User");
if (self) currentUserId = self._id;
for (const user of data.users) {
if (user._id && user.username) userNameCache[user._id] = user.username;
}
}
if (data.servers) {
for (const server of data.servers) {
if (server.roles) {
for (const role of Object.values(server.roles)) {
roleNameCache[role._id] = role.name;
}
}
}
}
}
if (data.type === "UserUpdate" && data.id && data.data?.username) {
userNameCache[data.id] = data.data.username;
}
if (data.type === "VoiceChannelJoin" && data.state?.id === currentUserId) {
@ -83,6 +110,17 @@ export function injectSounds(webContents: Electron.WebContents) {
playUserLeaveSound();
}
if (data.type === "Message" && data.role_mentions?.length > 0 && data.member?.roles?.length > 0) {
const matchedRoleId = data.role_mentions.find(roleId => data.member.roles.includes(roleId));
if (matchedRoleId) {
playMentionSound();
const senderName = userNameCache[data.author] || data.author || "Someone";
const roleName = roleNameCache[matchedRoleId] || matchedRoleId;
const content = data.content?.replace(/<%[^>]+>/g, '@' + roleName).trim() || "New message";
__showMentionToast(senderName, roleName, content);
}
}
} catch {}
});
}

126
src/native/toasts.ts Normal file
View File

@ -0,0 +1,126 @@
export const toastStyles = `
#__stoat-toast-container {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 99999;
display: flex;
flex-direction: column;
gap: 10px;
pointer-events: none;
}
.stoat-toast {
background: #1e1e2e;
border: 1px solid #3a3a55;
border-left: 4px solid #7c6af7;
border-radius: 8px;
padding: 12px 16px;
min-width: 280px;
max-width: 340px;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
pointer-events: all;
opacity: 0;
transform: translateX(20px);
transition: opacity 0.2s ease, transform 0.2s ease;
cursor: pointer;
}
.stoat-toast.visible {
opacity: 1;
transform: translateX(0);
}
.stoat-toast.hiding {
opacity: 0;
transform: translateX(20px);
}
.stoat-toast-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.stoat-toast-icon { font-size: 14px; }
.stoat-toast-sender {
font-size: 13px;
font-weight: 600;
color: #c9c9e0;
font-family: sans-serif;
}
.stoat-toast-role {
font-size: 11px;
background: #7c6af720;
color: #a89cf7;
border: 1px solid #7c6af740;
border-radius: 4px;
padding: 1px 6px;
font-family: sans-serif;
margin-left: auto;
}
.stoat-toast-content {
font-size: 12px;
color: #8888aa;
font-family: sans-serif;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 300px;
}
.stoat-toast-progress {
height: 2px;
background: #7c6af7;
border-radius: 2px;
margin-top: 10px;
animation: stoat-progress 5s linear forwards;
}
@keyframes stoat-progress {
from { width: 100%; }
to { width: 0%; }
}
`;
export const toastScript = `
function __initToasts() {
if (!document.getElementById('__stoat-toast-styles')) {
const style = document.createElement('style');
style.id = '__stoat-toast-styles';
style.textContent = ${JSON.stringify(toastStyles)};
document.head.appendChild(style);
}
}
function __getToastContainer() {
let container = document.getElementById('__stoat-toast-container');
if (!container) {
container = document.createElement('div');
container.id = '__stoat-toast-container';
document.body.appendChild(container);
}
return container;
}
function __dismissToast(toast) {
toast.classList.remove('visible');
toast.classList.add('hiding');
setTimeout(() => toast.remove(), 250);
}
function __showMentionToast(senderName, roleName, content) {
__initToasts();
const container = __getToastContainer();
const toast = document.createElement('div');
toast.className = 'stoat-toast';
toast.innerHTML =
'<div class="stoat-toast-header">' +
'<span class="stoat-toast-icon">🔔</span>' +
'<span class="stoat-toast-sender">' + senderName + '</span>' +
'<span class="stoat-toast-role">@' + roleName + '</span>' +
'</div>' +
'<div class="stoat-toast-content">' + content + '</div>' +
'<div class="stoat-toast-progress"></div>';
toast.addEventListener('click', () => __dismissToast(toast));
container.appendChild(toast);
requestAnimationFrame(() => {
requestAnimationFrame(() => toast.classList.add('visible'));
});
setTimeout(() => __dismissToast(toast), 5000);
}
`;