Skip to content

Commit 9b63103

Browse files
committed
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
1 parent 920236e commit 9b63103

File tree

6 files changed

+179
-0
lines changed

6 files changed

+179
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ Open `http://localhost:3000`. The dashboard updates in **Realtime** via Server-S
6363
* `/local <msg>` - Send message only to direct connections.
6464
* `/whisper <user> <msg>` - Send a private message.
6565
* `/block <user>` - Block a user.
66+
* `/timestamp` - Toggle message timestamps.
67+
* `/sound` - Toggle sound effects for sent/received messages.
6668
* **Easter Eggs:** `/shrug`, `/tableflip`, `/heart`, and more.
6769

6870
---

public/app.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,9 +281,26 @@ const promptEl = document.querySelector(".prompt");
281281
let myId = null;
282282
let myChatHistory = [];
283283
let globalChatEnabled = true;
284+
let showTimestamp = localStorage.getItem("showTimestamp") === "true";
284285
let blockedUsers = new Set(
285286
JSON.parse(localStorage.getItem("blockedUsers") || "[]")
286287
);
288+
289+
if (showTimestamp) {
290+
terminalOutput.classList.add("show-timestamps");
291+
}
292+
293+
window.toggleTimestamp = () => {
294+
showTimestamp = !showTimestamp;
295+
localStorage.setItem("showTimestamp", showTimestamp);
296+
if (showTimestamp) {
297+
terminalOutput.classList.add("show-timestamps");
298+
systemStatusBar.innerText = "[SYSTEM] Timestamps enabled";
299+
} else {
300+
terminalOutput.classList.remove("show-timestamps");
301+
systemStatusBar.innerText = "[SYSTEM] Timestamps disabled";
302+
}
303+
};
287304
let nameToId = new Map();
288305

289306
// Context Menu Logic
@@ -474,6 +491,17 @@ const appendMessage = (msg) => {
474491
let scopeLabel = msg.scope === "LOCAL" ? "[LOCAL] " : "";
475492
if (msg.target) scopeLabel = "[WHISPER] ";
476493

494+
const timestampSpan = document.createElement("span");
495+
timestampSpan.className = "timestamp";
496+
const date = new Date();
497+
timestampSpan.innerText = `[${date
498+
.getHours()
499+
.toString()
500+
.padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date
501+
.getSeconds()
502+
.toString()
503+
.padStart(2, "0")}]`;
504+
477505
const senderSpan = document.createElement("span");
478506
senderSpan.className = "msg-sender";
479507
senderSpan.style.color = senderColor;
@@ -504,6 +532,7 @@ const appendMessage = (msg) => {
504532
contentSpan.style.opacity = "0.8";
505533
}
506534

535+
div.appendChild(timestampSpan);
507536
div.appendChild(senderSpan);
508537
div.appendChild(contentSpan);
509538
}
@@ -513,6 +542,11 @@ const appendMessage = (msg) => {
513542
};
514543

515544
terminalInput.addEventListener("keypress", async (e) => {
545+
// Init audio context on first interaction
546+
if (window.SoundManager) {
547+
window.SoundManager.init();
548+
}
549+
516550
if (e.key === "Enter") {
517551
let content = terminalInput.value.trim();
518552
if (!content) return;
@@ -610,6 +644,7 @@ terminalInput.addEventListener("keypress", async (e) => {
610644
});
611645

612646
if (res.ok) {
647+
if (window.SoundManager) window.SoundManager.playSent();
613648
myChatHistory.push(Date.now());
614649
updatePromptStatus();
615650
} else if (res.status === 429) {
@@ -637,6 +672,14 @@ evtSource.onmessage = (event) => {
637672
}
638673

639674
if (data.type === "CHAT") {
675+
// Play sounds
676+
if (window.SoundManager && data.sender !== myId) {
677+
if (data.target === myId) {
678+
window.SoundManager.playWhisper();
679+
} else {
680+
window.SoundManager.playReceived();
681+
}
682+
}
640683
appendMessage(data);
641684
return;
642685
}

public/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<script src="/js/lists.js"></script>
2929
<script src="/js/screenname.js"></script>
3030
<script src="/js/commands.js"></script>
31+
<script src="/js/sound-manager.js"></script>
3132
</head>
3233
<body>
3334
<canvas id="network" data-visual-limit="{{VISUAL_LIMIT}}"></canvas>

public/js/commands.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,30 @@ const ChatCommands = {
1010
if (output) output.innerHTML = "";
1111
},
1212
},
13+
"/timestamp": {
14+
description: "Toggles timestamps on and off",
15+
execute: () => {
16+
if (window.toggleTimestamp) {
17+
window.toggleTimestamp();
18+
}
19+
},
20+
},
21+
"/sound": {
22+
description: "Toggles sound effects",
23+
execute: () => {
24+
if (window.SoundManager) {
25+
const enabled = window.SoundManager.toggle();
26+
const status = enabled ? "enabled" : "disabled";
27+
const output = document.getElementById("terminal-output");
28+
if (output) {
29+
const div = document.createElement("div");
30+
div.innerHTML = `<span style="color: #aaa">[SYSTEM] Sound effects ${status}</span>`;
31+
output.appendChild(div);
32+
output.scrollTop = output.scrollHeight;
33+
}
34+
}
35+
},
36+
},
1337
"/help": {
1438
description: "Shows available commands",
1539
execute: () => {
@@ -57,6 +81,8 @@ const ChatCommands = {
5781
desc: "Send message to direct peers only (Global by default)",
5882
},
5983
{ cmd: "/clear", desc: "Clear chat history" },
84+
{ cmd: "/timestamp", desc: "Toggle timestamps" },
85+
{ cmd: "/sound", desc: "Toggle sound effects" },
6086
{ cmd: "/help", desc: "Show this help menu" },
6187
];
6288
helpContainer.appendChild(

public/js/sound-manager.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
2+
class SoundManager {
3+
constructor() {
4+
this.ctx = null;
5+
this.enabled = localStorage.getItem("soundEnabled") === "true";
6+
}
7+
8+
init() {
9+
if (!this.ctx) {
10+
const AudioContext = window.AudioContext || window.webkitAudioContext;
11+
if (AudioContext) {
12+
this.ctx = new AudioContext();
13+
}
14+
}
15+
// Resume context if it's suspended (browsers auto-suspend)
16+
if (this.ctx && this.ctx.state === "suspended") {
17+
this.ctx.resume();
18+
}
19+
}
20+
21+
toggle() {
22+
this.enabled = !this.enabled;
23+
localStorage.setItem("soundEnabled", this.enabled);
24+
return this.enabled;
25+
}
26+
27+
// Helper to play a tone
28+
playTone(freq, type, duration, volume = 0.1) {
29+
if (!this.enabled) return;
30+
this.init();
31+
if (!this.ctx) return;
32+
33+
const osc = this.ctx.createOscillator();
34+
const gain = this.ctx.createGain();
35+
36+
osc.type = type;
37+
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
38+
39+
gain.gain.setValueAtTime(volume, this.ctx.currentTime);
40+
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration);
41+
42+
osc.connect(gain);
43+
gain.connect(this.ctx.destination);
44+
45+
osc.start();
46+
osc.stop(this.ctx.currentTime + duration);
47+
}
48+
49+
playSent() {
50+
// Satisfying "pop" or "click" for sending
51+
// Sine wave starting at 600Hz dropping fast
52+
if (!this.enabled) return;
53+
this.playTone(600, "sine", 0.15, 0.1);
54+
}
55+
56+
playReceived() {
57+
// Soft "bloop" for receiving
58+
// Sine wave starting at 400Hz
59+
if (!this.enabled) return;
60+
this.playTone(400, "sine", 0.15, 0.1);
61+
}
62+
63+
playWhisper() {
64+
// Distinct "ding" for whispers
65+
// Two tones slightly separated
66+
if (!this.enabled) return;
67+
this.init();
68+
if (!this.ctx) return;
69+
70+
const now = this.ctx.currentTime;
71+
72+
// Tone 1
73+
const osc1 = this.ctx.createOscillator();
74+
const gain1 = this.ctx.createGain();
75+
osc1.frequency.setValueAtTime(800, now);
76+
gain1.gain.setValueAtTime(0.1, now);
77+
gain1.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
78+
osc1.connect(gain1);
79+
gain1.connect(this.ctx.destination);
80+
osc1.start(now);
81+
osc1.stop(now + 0.3);
82+
83+
// Tone 2 (higher)
84+
const osc2 = this.ctx.createOscillator();
85+
const gain2 = this.ctx.createGain();
86+
osc2.frequency.setValueAtTime(1200, now + 0.1);
87+
gain2.gain.setValueAtTime(0.1, now + 0.1);
88+
gain2.gain.exponentialRampToValueAtTime(0.01, now + 0.4);
89+
osc2.connect(gain2);
90+
gain2.connect(this.ctx.destination);
91+
osc2.start(now + 0.1);
92+
osc2.stop(now + 0.4);
93+
}
94+
}
95+
96+
window.SoundManager = new SoundManager();

public/style.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,17 @@ a { color: var(--color-text-anchor-link); text-decoration: none; border-bottom:
338338
color: var(--color-terminal-output-message);
339339
}
340340

341+
.timestamp {
342+
display: none;
343+
color: #888;
344+
font-size: 0.8em;
345+
margin-right: 5px;
346+
}
347+
348+
.show-timestamps .timestamp {
349+
display: inline;
350+
}
351+
341352
/* Context Menu */
342353
.context-menu {
343354
display: none;

0 commit comments

Comments
 (0)