Merge PR #49 and resolve conflicts

This commit is contained in:
lklynet
2026-01-11 16:31:02 -05:00
9 changed files with 248 additions and 66 deletions
+2
View File
@@ -64,6 +64,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
@@ -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;
}
+2 -1
View File
@@ -24,6 +24,7 @@
<script src="/js/lists.js"></script>
<script src="/js/screenname.js"></script>
<script type="module" src="/js/commands.js"></script>
<script src="/js/sound-manager.js"></script>
<script src="/js/version-checker.js"></script>
</head>
@@ -144,4 +145,4 @@
<script src="/app.js"></script>
</body>
</html>
</html>
+65 -65
View File
@@ -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 = `<span style="color: #4ade80">${item.cmd}</span> - ${item.desc}`;
section.appendChild(div);
});
items.forEach((item) => {
const div = document.createElement("div");
div.innerHTML = `<span style="color: #4ade80">${item.cmd}</span> - ${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 &lt;user&gt; &lt;msg&gt;",
desc: "Send a private message",
},
{ cmd: "/block &lt;user&gt;", desc: "Block messages from a user" },
{ cmd: "/unblock &lt;user&gt;", desc: "Unblock a user" },
{
cmd: "/local &lt;msg&gt;",
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 &lt;user&gt; &lt;msg&gt;",
desc: "Send a private message",
},
{ cmd: "/block &lt;user&gt;", desc: "Block messages from a user" },
{ cmd: "/unblock &lt;user&gt;", desc: "Unblock a user" },
{
cmd: "/local &lt;msg&gt;",
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;
},
};
+16
View File
@@ -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 = `<span style="color: #aaa">[SYSTEM] Sound effects ${status}</span>`;
output.appendChild(div);
output.scrollTop = output.scrollHeight;
}
}
},
};
+8
View File
@@ -0,0 +1,8 @@
export const timestampCommand = {
description: "Toggles timestamps on and off",
execute: () => {
if (window.toggleTimestamp) {
window.toggleTimestamp();
}
},
};
+5
View File
@@ -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 };
+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;