fix: IRC SPA cleanup — /motd, /query, Firefox / key, default MOTD #58
@@ -13,6 +13,14 @@ import (
|
||||
_ "github.com/joho/godotenv/autoload" // loads .env file
|
||||
)
|
||||
|
||||
const defaultMOTD = ` _ __ ___ ___ (_)_ __ ___
|
||||
| '_ \ / _ \/ _ \ | | '__/ __|
|
||||
| | | | __/ (_) || | | | (__
|
||||
|_| |_|\___|\___/ |_|_| \___|
|
||||
|
||||
Welcome to NeoIRC — IRC semantics over HTTP.
|
||||
Type /help for available commands.`
|
||||
|
||||
// Params defines the dependencies for creating a Config.
|
||||
type Params struct {
|
||||
fx.In
|
||||
@@ -62,7 +70,7 @@ func New(
|
||||
viper.SetDefault("METRICS_PASSWORD", "")
|
||||
viper.SetDefault("MAX_HISTORY", "10000")
|
||||
viper.SetDefault("MAX_MESSAGE_SIZE", "4096")
|
||||
viper.SetDefault("MOTD", "")
|
||||
viper.SetDefault("MOTD", defaultMOTD)
|
||||
viper.SetDefault("SERVER_NAME", "")
|
||||
viper.SetDefault("FEDERATION_KEY", "")
|
||||
viper.SetDefault("SESSION_IDLE_TIMEOUT", "24h")
|
||||
|
||||
@@ -670,6 +670,13 @@ func (hdlr *Handlers) dispatchCommand(
|
||||
hdlr.handleQuit(
|
||||
writer, request, sessionID, nick, body,
|
||||
)
|
||||
case "MOTD":
|
||||
hdlr.deliverMOTD(
|
||||
request, clientID, sessionID, nick,
|
||||
)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
case "PING":
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{
|
||||
|
||||
113
web/src/app.jsx
113
web/src/app.jsx
@@ -71,7 +71,7 @@ function LoginScreen({ onLogin }) {
|
||||
const saved = localStorage.getItem("neoirc_token");
|
||||
if (saved) {
|
||||
api("/state")
|
||||
.then((u) => onLogin(u.nick))
|
||||
.then((u) => onLogin(u.nick, true))
|
||||
.catch(() => localStorage.removeItem("neoirc_token"));
|
||||
}
|
||||
inputRef.current?.focus();
|
||||
@@ -497,6 +497,31 @@ function App() {
|
||||
inputRef.current?.focus();
|
||||
}, [activeTab]);
|
||||
|
||||
// Global keyboard handler — capture '/' to prevent
|
||||
// Firefox quick search and redirect focus to the input.
|
||||
useEffect(() => {
|
||||
const handleGlobalKeyDown = (e) => {
|
||||
if (
|
||||
e.key === "/" &&
|
||||
document.activeElement !== inputRef.current &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey &&
|
||||
!e.metaKey
|
||||
) {
|
||||
e.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleGlobalKeyDown);
|
||||
|
||||
// Also focus input on initial mount.
|
||||
inputRef.current?.focus();
|
||||
|
||||
return () =>
|
||||
document.removeEventListener("keydown", handleGlobalKeyDown);
|
||||
}, []);
|
||||
|
||||
// Fetch topic for active channel.
|
||||
useEffect(() => {
|
||||
if (!loggedIn) return;
|
||||
@@ -512,10 +537,24 @@ function App() {
|
||||
}, [loggedIn, activeTab, tabs]);
|
||||
|
||||
const onLogin = useCallback(
|
||||
async (userNick) => {
|
||||
async (userNick, isResumed) => {
|
||||
setNick(userNick);
|
||||
setLoggedIn(true);
|
||||
addSystemMessage("Server", `Connected as ${userNick}`);
|
||||
|
||||
// Request MOTD on resumed sessions (new sessions get
|
||||
// it automatically from the server during creation).
|
||||
if (isResumed) {
|
||||
try {
|
||||
await api("/messages", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ command: "MOTD" }),
|
||||
});
|
||||
} catch (e) {
|
||||
// MOTD is non-critical.
|
||||
}
|
||||
}
|
||||
|
||||
const saved = JSON.parse(
|
||||
localStorage.getItem("neoirc_channels") || "[]",
|
||||
);
|
||||
@@ -791,18 +830,70 @@ function App() {
|
||||
|
||||
break;
|
||||
}
|
||||
case "/motd": {
|
||||
try {
|
||||
await api("/messages", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ command: "MOTD" }),
|
||||
});
|
||||
} catch (err) {
|
||||
addSystemMessage(
|
||||
"Server",
|
||||
`Failed to request MOTD: ${err.data?.error || "error"}`,
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "/query": {
|
||||
if (parts[1]) {
|
||||
const target = parts[1];
|
||||
openDM(target);
|
||||
const msgText = parts.slice(2).join(" ");
|
||||
if (msgText) {
|
||||
try {
|
||||
await api("/messages", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
command: "PRIVMSG",
|
||||
to: target,
|
||||
body: [msgText],
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
addSystemMessage(
|
||||
"Server",
|
||||
`Message failed: ${err.data?.error || "error"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addSystemMessage("Server", "Usage: /query <nick> [message]");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "/clear": {
|
||||
const clearTarget = tab.name;
|
||||
setMessages((prev) => ({ ...prev, [clearTarget]: [] }));
|
||||
|
||||
break;
|
||||
}
|
||||
case "/help": {
|
||||
const helpLines = [
|
||||
"Available commands:",
|
||||
" /join #channel — Join a channel",
|
||||
" /part [reason] — Part the current channel",
|
||||
" /msg nick message — Send a private message",
|
||||
" /me action — Send an action",
|
||||
" /nick newnick — Change your nickname",
|
||||
" /topic [text] — View or set channel topic",
|
||||
" /mode +/-flags — Set channel modes",
|
||||
" /quit [reason] — Disconnect from server",
|
||||
" /help — Show this help",
|
||||
" /join #channel — Join a channel",
|
||||
" /part [reason] — Part the current channel",
|
||||
" /msg nick message — Send a private message",
|
||||
" /query nick [message] — Open a DM tab (optionally send a message)",
|
||||
" /me action — Send an action",
|
||||
" /nick newnick — Change your nickname",
|
||||
" /topic [text] — View or set channel topic",
|
||||
" /mode +/-flags — Set channel modes",
|
||||
" /motd — Display the message of the day",
|
||||
" /clear — Clear messages in the current tab",
|
||||
" /quit [reason] — Disconnect from server",
|
||||
" /help — Show this help",
|
||||
];
|
||||
for (const line of helpLines) {
|
||||
addSystemMessage("Server", line);
|
||||
|
||||
@@ -70,14 +70,14 @@ body,
|
||||
}
|
||||
|
||||
.login-box .motd {
|
||||
color: var(--text-dim);
|
||||
font-size: 12px;
|
||||
color: var(--accent);
|
||||
font-size: 11px;
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
white-space: pre-wrap;
|
||||
white-space: pre;
|
||||
font-family: inherit;
|
||||
border-left: 2px solid var(--border);
|
||||
padding-left: 12px;
|
||||
line-height: 1.2;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.login-box form {
|
||||
|
||||
Reference in New Issue
Block a user