fix: IRC SPA cleanup — /motd, /query, Firefox / key, default MOTD
All checks were successful
check / check (push) Successful in 6s
All checks were successful
check / check (push) Successful in 6s
- Add default MOTD with figlet-style ASCII art for neoirc - Add MOTD server command so /motd can be re-requested - Add /motd client command to display message of the day - Add /query command to open DM tabs with optional message - Add /clear command to clear messages in current tab - Fix Firefox quick search conflict: global keydown handler captures '/' and redirects focus to the input element - Auto-focus input on SPA init and tab changes - Show MOTD on resumed sessions (new sessions get it from server automatically during creation) - Update /help with new commands - Style MOTD on login screen for better ASCII art display
This commit is contained in:
@@ -13,6 +13,14 @@ import (
|
|||||||
_ "github.com/joho/godotenv/autoload" // loads .env file
|
_ "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.
|
// Params defines the dependencies for creating a Config.
|
||||||
type Params struct {
|
type Params struct {
|
||||||
fx.In
|
fx.In
|
||||||
@@ -62,7 +70,7 @@ func New(
|
|||||||
viper.SetDefault("METRICS_PASSWORD", "")
|
viper.SetDefault("METRICS_PASSWORD", "")
|
||||||
viper.SetDefault("MAX_HISTORY", "10000")
|
viper.SetDefault("MAX_HISTORY", "10000")
|
||||||
viper.SetDefault("MAX_MESSAGE_SIZE", "4096")
|
viper.SetDefault("MAX_MESSAGE_SIZE", "4096")
|
||||||
viper.SetDefault("MOTD", "")
|
viper.SetDefault("MOTD", defaultMOTD)
|
||||||
viper.SetDefault("SERVER_NAME", "")
|
viper.SetDefault("SERVER_NAME", "")
|
||||||
viper.SetDefault("FEDERATION_KEY", "")
|
viper.SetDefault("FEDERATION_KEY", "")
|
||||||
viper.SetDefault("SESSION_IDLE_TIMEOUT", "24h")
|
viper.SetDefault("SESSION_IDLE_TIMEOUT", "24h")
|
||||||
|
|||||||
@@ -670,6 +670,13 @@ func (hdlr *Handlers) dispatchCommand(
|
|||||||
hdlr.handleQuit(
|
hdlr.handleQuit(
|
||||||
writer, request, sessionID, nick, body,
|
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":
|
case "PING":
|
||||||
hdlr.respondJSON(writer, request,
|
hdlr.respondJSON(writer, request,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ function LoginScreen({ onLogin }) {
|
|||||||
const saved = localStorage.getItem("neoirc_token");
|
const saved = localStorage.getItem("neoirc_token");
|
||||||
if (saved) {
|
if (saved) {
|
||||||
api("/state")
|
api("/state")
|
||||||
.then((u) => onLogin(u.nick))
|
.then((u) => onLogin(u.nick, true))
|
||||||
.catch(() => localStorage.removeItem("neoirc_token"));
|
.catch(() => localStorage.removeItem("neoirc_token"));
|
||||||
}
|
}
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
@@ -497,6 +497,31 @@ function App() {
|
|||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}, [activeTab]);
|
}, [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.
|
// Fetch topic for active channel.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loggedIn) return;
|
if (!loggedIn) return;
|
||||||
@@ -512,10 +537,24 @@ function App() {
|
|||||||
}, [loggedIn, activeTab, tabs]);
|
}, [loggedIn, activeTab, tabs]);
|
||||||
|
|
||||||
const onLogin = useCallback(
|
const onLogin = useCallback(
|
||||||
async (userNick) => {
|
async (userNick, isResumed) => {
|
||||||
setNick(userNick);
|
setNick(userNick);
|
||||||
setLoggedIn(true);
|
setLoggedIn(true);
|
||||||
addSystemMessage("Server", `Connected as ${userNick}`);
|
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(
|
const saved = JSON.parse(
|
||||||
localStorage.getItem("neoirc_channels") || "[]",
|
localStorage.getItem("neoirc_channels") || "[]",
|
||||||
);
|
);
|
||||||
@@ -791,16 +830,68 @@ function App() {
|
|||||||
|
|
||||||
break;
|
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": {
|
case "/help": {
|
||||||
const helpLines = [
|
const helpLines = [
|
||||||
"Available commands:",
|
"Available commands:",
|
||||||
" /join #channel — Join a channel",
|
" /join #channel — Join a channel",
|
||||||
" /part [reason] — Part the current channel",
|
" /part [reason] — Part the current channel",
|
||||||
" /msg nick message — Send a private message",
|
" /msg nick message — Send a private message",
|
||||||
|
" /query nick [message] — Open a DM tab (optionally send a message)",
|
||||||
" /me action — Send an action",
|
" /me action — Send an action",
|
||||||
" /nick newnick — Change your nickname",
|
" /nick newnick — Change your nickname",
|
||||||
" /topic [text] — View or set channel topic",
|
" /topic [text] — View or set channel topic",
|
||||||
" /mode +/-flags — Set channel modes",
|
" /mode +/-flags — Set channel modes",
|
||||||
|
" /motd — Display the message of the day",
|
||||||
|
" /clear — Clear messages in the current tab",
|
||||||
" /quit [reason] — Disconnect from server",
|
" /quit [reason] — Disconnect from server",
|
||||||
" /help — Show this help",
|
" /help — Show this help",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -70,14 +70,14 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-box .motd {
|
.login-box .motd {
|
||||||
color: var(--text-dim);
|
color: var(--accent);
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
white-space: pre-wrap;
|
white-space: pre;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
border-left: 2px solid var(--border);
|
line-height: 1.2;
|
||||||
padding-left: 12px;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-box form {
|
.login-box form {
|
||||||
|
|||||||
Reference in New Issue
Block a user