feat(chat): add sound effects and timestamp toggle functionality

- Implement SoundManager for playing sound effects on message events
- Add /sound and /timestamp commands to toggle features
- Persist sound and timestamp preferences in localStorage
- Style timestamps in chat messages
This commit is contained in:
lklynet
2026-01-08 15:58:37 -05:00
parent 920236ed13
commit 9b63103148
6 changed files with 179 additions and 0 deletions
+2
View File
@@ -63,6 +63,8 @@ Open `http://localhost:3000`. The dashboard updates in **Realtime** via Server-S
* `/local <msg>` - Send message only to direct connections.
* `/whisper <user> <msg>` - Send a private message.
* `/block <user>` - Block a user.
* `/timestamp` - Toggle message timestamps.
* `/sound` - Toggle sound effects for sent/received messages.
* **Easter Eggs:** `/shrug`, `/tableflip`, `/heart`, and more.
---
+43
View File
@@ -281,9 +281,26 @@ const promptEl = document.querySelector(".prompt");
let myId = null;
let myChatHistory = [];
let globalChatEnabled = true;
let showTimestamp = localStorage.getItem("showTimestamp") === "true";
let blockedUsers = new Set(
JSON.parse(localStorage.getItem("blockedUsers") || "[]")
);
if (showTimestamp) {
terminalOutput.classList.add("show-timestamps");
}
window.toggleTimestamp = () => {
showTimestamp = !showTimestamp;
localStorage.setItem("showTimestamp", showTimestamp);
if (showTimestamp) {
terminalOutput.classList.add("show-timestamps");
systemStatusBar.innerText = "[SYSTEM] Timestamps enabled";
} else {
terminalOutput.classList.remove("show-timestamps");
systemStatusBar.innerText = "[SYSTEM] Timestamps disabled";
}
};
let nameToId = new Map();
// Context Menu Logic
@@ -474,6 +491,17 @@ const appendMessage = (msg) => {
let scopeLabel = msg.scope === "LOCAL" ? "[LOCAL] " : "";
if (msg.target) scopeLabel = "[WHISPER] ";
const timestampSpan = document.createElement("span");
timestampSpan.className = "timestamp";
const date = new Date();
timestampSpan.innerText = `[${date
.getHours()
.toString()
.padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date
.getSeconds()
.toString()
.padStart(2, "0")}]`;
const senderSpan = document.createElement("span");
senderSpan.className = "msg-sender";
senderSpan.style.color = senderColor;
@@ -504,6 +532,7 @@ const appendMessage = (msg) => {
contentSpan.style.opacity = "0.8";
}
div.appendChild(timestampSpan);
div.appendChild(senderSpan);
div.appendChild(contentSpan);
}
@@ -513,6 +542,11 @@ const appendMessage = (msg) => {
};
terminalInput.addEventListener("keypress", async (e) => {
// Init audio context on first interaction
if (window.SoundManager) {
window.SoundManager.init();
}
if (e.key === "Enter") {
let content = terminalInput.value.trim();
if (!content) return;
@@ -610,6 +644,7 @@ terminalInput.addEventListener("keypress", async (e) => {
});
if (res.ok) {
if (window.SoundManager) window.SoundManager.playSent();
myChatHistory.push(Date.now());
updatePromptStatus();
} else if (res.status === 429) {
@@ -637,6 +672,14 @@ evtSource.onmessage = (event) => {
}
if (data.type === "CHAT") {
// Play sounds
if (window.SoundManager && data.sender !== myId) {
if (data.target === myId) {
window.SoundManager.playWhisper();
} else {
window.SoundManager.playReceived();
}
}
appendMessage(data);
return;
}
+1
View File
@@ -28,6 +28,7 @@
<script src="/js/lists.js"></script>
<script src="/js/screenname.js"></script>
<script src="/js/commands.js"></script>
<script src="/js/sound-manager.js"></script>
</head>
<body>
<canvas id="network" data-visual-limit="{{VISUAL_LIMIT}}"></canvas>
+26
View File
@@ -10,6 +10,30 @@ const ChatCommands = {
if (output) output.innerHTML = "";
},
},
"/timestamp": {
description: "Toggles timestamps on and off",
execute: () => {
if (window.toggleTimestamp) {
window.toggleTimestamp();
}
},
},
"/sound": {
description: "Toggles sound effects",
execute: () => {
if (window.SoundManager) {
const enabled = window.SoundManager.toggle();
const status = enabled ? "enabled" : "disabled";
const output = document.getElementById("terminal-output");
if (output) {
const div = document.createElement("div");
div.innerHTML = `<span style="color: #aaa">[SYSTEM] Sound effects ${status}</span>`;
output.appendChild(div);
output.scrollTop = output.scrollHeight;
}
}
},
},
"/help": {
description: "Shows available commands",
execute: () => {
@@ -57,6 +81,8 @@ const ChatCommands = {
desc: "Send message to direct peers only (Global by default)",
},
{ cmd: "/clear", desc: "Clear chat history" },
{ cmd: "/timestamp", desc: "Toggle timestamps" },
{ cmd: "/sound", desc: "Toggle sound effects" },
{ cmd: "/help", desc: "Show this help menu" },
];
helpContainer.appendChild(
+96
View File
@@ -0,0 +1,96 @@
class SoundManager {
constructor() {
this.ctx = null;
this.enabled = localStorage.getItem("soundEnabled") === "true";
}
init() {
if (!this.ctx) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (AudioContext) {
this.ctx = new AudioContext();
}
}
// Resume context if it's suspended (browsers auto-suspend)
if (this.ctx && this.ctx.state === "suspended") {
this.ctx.resume();
}
}
toggle() {
this.enabled = !this.enabled;
localStorage.setItem("soundEnabled", this.enabled);
return this.enabled;
}
// Helper to play a tone
playTone(freq, type, duration, volume = 0.1) {
if (!this.enabled) return;
this.init();
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
gain.gain.setValueAtTime(volume, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + duration);
}
playSent() {
// Satisfying "pop" or "click" for sending
// Sine wave starting at 600Hz dropping fast
if (!this.enabled) return;
this.playTone(600, "sine", 0.15, 0.1);
}
playReceived() {
// Soft "bloop" for receiving
// Sine wave starting at 400Hz
if (!this.enabled) return;
this.playTone(400, "sine", 0.15, 0.1);
}
playWhisper() {
// Distinct "ding" for whispers
// Two tones slightly separated
if (!this.enabled) return;
this.init();
if (!this.ctx) return;
const now = this.ctx.currentTime;
// Tone 1
const osc1 = this.ctx.createOscillator();
const gain1 = this.ctx.createGain();
osc1.frequency.setValueAtTime(800, now);
gain1.gain.setValueAtTime(0.1, now);
gain1.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc1.connect(gain1);
gain1.connect(this.ctx.destination);
osc1.start(now);
osc1.stop(now + 0.3);
// Tone 2 (higher)
const osc2 = this.ctx.createOscillator();
const gain2 = this.ctx.createGain();
osc2.frequency.setValueAtTime(1200, now + 0.1);
gain2.gain.setValueAtTime(0.1, now + 0.1);
gain2.gain.exponentialRampToValueAtTime(0.01, now + 0.4);
osc2.connect(gain2);
gain2.connect(this.ctx.destination);
osc2.start(now + 0.1);
osc2.stop(now + 0.4);
}
}
window.SoundManager = new SoundManager();
+11
View File
@@ -338,6 +338,17 @@ a { color: var(--color-text-anchor-link); text-decoration: none; border-bottom:
color: var(--color-terminal-output-message);
}
.timestamp {
display: none;
color: #888;
font-size: 0.8em;
margin-right: 5px;
}
.show-timestamps .timestamp {
display: inline;
}
/* Context Menu */
.context-menu {
display: none;