mirror of
https://github.com/lklynet/hypermind.git
synced 2026-05-03 09:30:36 +00:00
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:
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user