diff --git a/README.md b/README.md index f94b96d..516f81f 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ Open `http://localhost:3000`. The dashboard updates in **Realtime** via Server-S * `/local ` - Send message only to direct connections. * `/whisper ` - Send a private message. * `/block ` - Block a user. +* `/timestamp` - Toggle message timestamps. +* `/sound` - Toggle sound effects for sent/received messages. * **Easter Eggs:** `/shrug`, `/tableflip`, `/heart`, and more. --- diff --git a/public/app.js b/public/app.js index 5397171..59a4194 100644 --- a/public/app.js +++ b/public/app.js @@ -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 @@ -487,6 +504,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; @@ -517,6 +545,7 @@ const appendMessage = (msg) => { contentSpan.style.opacity = "0.8"; } + div.appendChild(timestampSpan); div.appendChild(senderSpan); div.appendChild(contentSpan); } @@ -526,6 +555,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; @@ -635,6 +669,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) { @@ -662,6 +697,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; } diff --git a/public/index.html b/public/index.html index 1bc8b04..f78d709 100644 --- a/public/index.html +++ b/public/index.html @@ -24,6 +24,7 @@ + @@ -144,4 +145,4 @@ - \ No newline at end of file + diff --git a/public/js/chat-commands/help.js b/public/js/chat-commands/help.js index bbf9132..f9e74ac 100644 --- a/public/js/chat-commands/help.js +++ b/public/js/chat-commands/help.js @@ -1,76 +1,76 @@ export const helpCommand = { - description: "Shows available commands", - execute: () => { - const output = document.getElementById("terminal-output"); - if (!output) return; + description: "Shows available commands", + execute: () => { + const output = document.getElementById("terminal-output"); + if (!output) return; - const createHelpSection = (title, items) => { - const section = document.createElement("div"); - section.style.marginBottom = "10px"; - section.style.color = "#aaa"; + const createHelpSection = (title, items) => { + const section = document.createElement("div"); + section.style.marginBottom = "10px"; + section.style.color = "#aaa"; - const header = document.createElement("div"); - header.style.fontWeight = "bold"; - header.style.color = "#fff"; - header.style.marginBottom = "4px"; - header.innerText = title; - section.appendChild(header); + const header = document.createElement("div"); + header.style.fontWeight = "bold"; + header.style.color = "#fff"; + header.style.marginBottom = "4px"; + header.innerText = title; + section.appendChild(header); - items.forEach((item) => { - const div = document.createElement("div"); - div.innerHTML = `${item.cmd} - ${item.desc}`; - section.appendChild(div); - }); + items.forEach((item) => { + const div = document.createElement("div"); + div.innerHTML = `${item.cmd} - ${item.desc}`; + section.appendChild(div); + }); - return section; - }; + return section; + }; - const helpContainer = document.createElement("div"); - helpContainer.className = "system-message"; - helpContainer.style.padding = "10px"; - helpContainer.style.borderTop = "1px dashed #333"; - helpContainer.style.borderBottom = "1px dashed #333"; - helpContainer.style.margin = "10px 0"; + const helpContainer = document.createElement("div"); + helpContainer.className = "system-message"; + helpContainer.style.padding = "10px"; + helpContainer.style.borderTop = "1px dashed #333"; + helpContainer.style.borderBottom = "1px dashed #333"; + helpContainer.style.margin = "10px 0"; - // system - const systemCmds = [ - { - cmd: "/whisper <user> <msg>", - desc: "Send a private message", - }, - { cmd: "/block <user>", desc: "Block messages from a user" }, - { cmd: "/unblock <user>", desc: "Unblock a user" }, - { - cmd: "/local <msg>", - desc: "Send message to direct peers only (Global by default)", - }, - { cmd: "/clear", desc: "Clear chat history" }, - { cmd: "/help", desc: "Show this help menu" }, - ]; - helpContainer.appendChild( - createHelpSection("System Commands", systemCmds) - ); + // system + const systemCmds = [ + { + cmd: "/whisper <user> <msg>", + desc: "Send a private message", + }, + { cmd: "/block <user>", desc: "Block messages from a user" }, + { cmd: "/unblock <user>", desc: "Unblock a user" }, + { + cmd: "/local <msg>", + 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(createHelpSection("System Commands", systemCmds)); - // Formatting - const formatCmds = [ - { cmd: "**text**", desc: "Bold" }, - { cmd: "*text*", desc: "Italics" }, - { cmd: "__text__", desc: "Underline" }, - { cmd: "~~text~~", desc: "Strikethrough" }, - { cmd: "`text`", desc: "Code" }, - ]; - helpContainer.appendChild(createHelpSection("Formatting", formatCmds)); + // Formatting + const formatCmds = [ + { cmd: "**text**", desc: "Bold" }, + { cmd: "*text*", desc: "Italics" }, + { cmd: "__text__", desc: "Underline" }, + { cmd: "~~text~~", desc: "Strikethrough" }, + { cmd: "`text`", desc: "Code" }, + ]; + helpContainer.appendChild(createHelpSection("Formatting", formatCmds)); - // Easter Eggs - const eggs = Object.entries(window.ChatCommands.replacements).map( - ([k, v]) => ({ - cmd: k, - desc: v, - }) - ); - helpContainer.appendChild(createHelpSection("Easter Eggs", eggs)); + // Easter Eggs + const eggs = Object.entries(window.ChatCommands.replacements).map( + ([k, v]) => ({ + cmd: k, + desc: v, + }) + ); + helpContainer.appendChild(createHelpSection("Easter Eggs", eggs)); - output.appendChild(helpContainer); - output.scrollTop = output.scrollHeight; - }, + output.appendChild(helpContainer); + output.scrollTop = output.scrollHeight; + }, }; diff --git a/public/js/chat-commands/sound.js b/public/js/chat-commands/sound.js new file mode 100644 index 0000000..7753f7e --- /dev/null +++ b/public/js/chat-commands/sound.js @@ -0,0 +1,16 @@ +export const soundCommand = { + 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 = `[SYSTEM] Sound effects ${status}`; + output.appendChild(div); + output.scrollTop = output.scrollHeight; + } + } + }, +}; diff --git a/public/js/chat-commands/timestamp.js b/public/js/chat-commands/timestamp.js new file mode 100644 index 0000000..d0800a7 --- /dev/null +++ b/public/js/chat-commands/timestamp.js @@ -0,0 +1,8 @@ +export const timestampCommand = { + description: "Toggles timestamps on and off", + execute: () => { + if (window.toggleTimestamp) { + window.toggleTimestamp(); + } + }, +}; diff --git a/public/js/commands.js b/public/js/commands.js index 268c37a..5ef9ae5 100644 --- a/public/js/commands.js +++ b/public/js/commands.js @@ -6,6 +6,8 @@ import { clearCommand } from "./chat-commands/clear.js"; import { frenzyCommand } from "./chat-commands/frenzy.js"; import { helpCommand } from "./chat-commands/help.js"; import { replacements } from "./chat-commands/replacements.js"; +import { timestampCommand } from "./chat-commands/timestamp.js"; +import { soundCommand } from "./chat-commands/sound.js"; const formatMessage = (text) => { if (!text) return ""; @@ -30,6 +32,8 @@ const actions = { "/clear": clearCommand, "/frenzy": frenzyCommand, "/help": helpCommand, + "/timestamp": timestampCommand, + "/sound": soundCommand, }; const processInput = (input) => { @@ -54,3 +58,4 @@ const ChatCommands = { }; window.ChatCommands = ChatCommands; +export { ChatCommands }; diff --git a/public/js/sound-manager.js b/public/js/sound-manager.js new file mode 100644 index 0000000..c19e639 --- /dev/null +++ b/public/js/sound-manager.js @@ -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(); diff --git a/public/style.css b/public/style.css index 3f16d9a..4f14e49 100644 --- a/public/style.css +++ b/public/style.css @@ -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;