fix: IRC SPA cleanup — /motd, /query, Firefox / key, default MOTD #58
No files matched your search
+110
-4
@@ -138,16 +138,29 @@ func (a *App) dispatchCommand(cmd, args string) {
|
||||
a.cmdQuery(args)
|
||||
case "/topic":
|
||||
a.cmdTopic(args)
|
||||
case "/names":
|
||||
a.cmdNames()
|
||||
case "/list":
|
||||
a.cmdList()
|
||||
case "/window", "/w":
|
||||
a.cmdWindow(args)
|
||||
case "/quit":
|
||||
a.cmdQuit()
|
||||
case "/help":
|
||||
a.cmdHelp()
|
||||
default:
|
||||
a.dispatchInfoCommand(cmd, args)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) dispatchInfoCommand(cmd, args string) {
|
||||
switch cmd {
|
||||
case "/names":
|
||||
a.cmdNames()
|
||||
case "/list":
|
||||
a.cmdList()
|
||||
case "/motd":
|
||||
a.cmdMotd()
|
||||
case "/who":
|
||||
a.cmdWho(args)
|
||||
case "/whois":
|
||||
a.cmdWhois(args)
|
||||
default:
|
||||
a.ui.AddStatus(
|
||||
"[red]Unknown command: " + cmd,
|
||||
@@ -510,6 +523,96 @@ func (a *App) cmdList() {
|
||||
a.ui.AddStatus("[cyan]*** End of channel list")
|
||||
}
|
||||
|
||||
func (a *App) cmdMotd() {
|
||||
a.mu.Lock()
|
||||
connected := a.connected
|
||||
a.mu.Unlock()
|
||||
|
||||
if !connected {
|
||||
a.ui.AddStatus("[red]Not connected")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err := a.client.SendMessage(
|
||||
&api.Message{Command: "MOTD"}, //nolint:exhaustruct
|
||||
)
|
||||
if err != nil {
|
||||
a.ui.AddStatus(fmt.Sprintf(
|
||||
"[red]MOTD failed: %v", err,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) cmdWho(args string) {
|
||||
a.mu.Lock()
|
||||
connected := a.connected
|
||||
target := a.target
|
||||
a.mu.Unlock()
|
||||
|
||||
if !connected {
|
||||
a.ui.AddStatus("[red]Not connected")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
channel := args
|
||||
if channel == "" {
|
||||
channel = target
|
||||
}
|
||||
|
||||
if channel == "" ||
|
||||
!strings.HasPrefix(channel, "#") {
|
||||
a.ui.AddStatus(
|
||||
"[red]Usage: /who #channel",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err := a.client.SendMessage(
|
||||
&api.Message{ //nolint:exhaustruct
|
||||
Command: "WHO", To: channel,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
a.ui.AddStatus(fmt.Sprintf(
|
||||
"[red]WHO failed: %v", err,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) cmdWhois(args string) {
|
||||
a.mu.Lock()
|
||||
connected := a.connected
|
||||
a.mu.Unlock()
|
||||
|
||||
if !connected {
|
||||
a.ui.AddStatus("[red]Not connected")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if args == "" {
|
||||
a.ui.AddStatus(
|
||||
"[red]Usage: /whois <nick>",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err := a.client.SendMessage(
|
||||
&api.Message{ //nolint:exhaustruct
|
||||
Command: "WHOIS", To: args,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
a.ui.AddStatus(fmt.Sprintf(
|
||||
"[red]WHOIS failed: %v", err,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) cmdWindow(args string) {
|
||||
if args == "" {
|
||||
a.ui.AddStatus(
|
||||
@@ -574,6 +677,9 @@ func (a *App) cmdHelp() {
|
||||
" /topic [text] — View/set topic",
|
||||
" /names — List channel members",
|
||||
" /list — List channels",
|
||||
" /who [#channel] — List users in channel",
|
||||
" /whois <nick> — Show user info",
|
||||
" /motd — Show message of the day",
|
||||
" /window <n> — Switch buffer",
|
||||
" /quit — Disconnect and exit",
|
||||
" /help — This help",
|
||||
|
||||
@@ -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")
|
||||
|
||||
+269
-7
@@ -670,13 +670,12 @@ func (hdlr *Handlers) dispatchCommand(
|
||||
hdlr.handleQuit(
|
||||
writer, request, sessionID, nick, body,
|
||||
)
|
||||
case "PING":
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{
|
||||
"command": "PONG",
|
||||
"from": hdlr.serverName(),
|
||||
},
|
||||
http.StatusOK)
|
||||
case "MOTD", "LIST", "WHO", "WHOIS", "PING":
|
||||
hdlr.dispatchInfoCommand(
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
command, target, bodyLines,
|
||||
)
|
||||
default:
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
@@ -1391,6 +1390,269 @@ func (hdlr *Handlers) executeTopic(
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// dispatchInfoCommand handles informational IRC commands
|
||||
// that produce server-side numerics (MOTD, LIST, WHO,
|
||||
// WHOIS, PING).
|
||||
func (hdlr *Handlers) dispatchInfoCommand(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, command, target string,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
okResp := map[string]string{"status": "ok"}
|
||||
|
||||
switch command {
|
||||
case "MOTD":
|
||||
hdlr.deliverMOTD(
|
||||
request, clientID, sessionID, nick,
|
||||
)
|
||||
case "LIST":
|
||||
hdlr.handleListCmd(
|
||||
request, clientID, sessionID, nick,
|
||||
)
|
||||
case "WHO":
|
||||
hdlr.handleWhoCmd(
|
||||
request, clientID, sessionID, nick,
|
||||
target,
|
||||
)
|
||||
case "WHOIS":
|
||||
hdlr.handleWhoisCmd(
|
||||
request, clientID, sessionID, nick,
|
||||
target, bodyLines,
|
||||
)
|
||||
case "PING":
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{
|
||||
"command": "PONG",
|
||||
"from": hdlr.serverName(),
|
||||
},
|
||||
http.StatusOK)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.respondJSON(
|
||||
writer, request, okResp, http.StatusOK,
|
||||
)
|
||||
}
|
||||
|
||||
// handleListCmd sends RPL_LIST (322) for each channel,
|
||||
// then sends 323 to signal the end of the list.
|
||||
func (hdlr *Handlers) handleListCmd(
|
||||
request *http.Request,
|
||||
clientID, sessionID int64,
|
||||
nick string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
|
||||
channels, err := hdlr.params.Database.ListAllChannels(
|
||||
ctx,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "323", nick, nil,
|
||||
"End of /LIST",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for _, channel := range channels {
|
||||
memberIDs, _ :=
|
||||
hdlr.params.Database.GetChannelMemberIDs(
|
||||
ctx, channel.ID,
|
||||
)
|
||||
|
||||
count := strconv.Itoa(len(memberIDs))
|
||||
topic := channel.Topic
|
||||
|
||||
if topic == "" {
|
||||
topic = " "
|
||||
}
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "322", nick,
|
||||
[]string{channel.Name, count}, topic,
|
||||
)
|
||||
}
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "323", nick, nil,
|
||||
"End of /LIST",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
}
|
||||
|
||||
// handleWhoCmd sends RPL_WHOREPLY (352) for each member
|
||||
// of the target channel, followed by RPL_ENDOFWHO (315).
|
||||
func (hdlr *Handlers) handleWhoCmd(
|
||||
request *http.Request,
|
||||
clientID, sessionID int64,
|
||||
nick, target string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
|
||||
if target == "" {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "461", nick,
|
||||
[]string{"WHO"}, "Not enough parameters",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
channel := target
|
||||
if !strings.HasPrefix(channel, "#") {
|
||||
channel = "#" + channel
|
||||
}
|
||||
|
||||
chID, err := hdlr.params.Database.GetChannelByName(
|
||||
ctx, channel,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "315", nick,
|
||||
[]string{channel}, "End of /WHO list",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
members, err := hdlr.params.Database.ChannelMembers(
|
||||
ctx, chID,
|
||||
)
|
||||
if err == nil {
|
||||
srvName := hdlr.serverName()
|
||||
|
||||
for _, mem := range members {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "352", nick,
|
||||
[]string{
|
||||
channel, mem.Nick, "neoirc",
|
||||
srvName, mem.Nick, "H",
|
||||
},
|
||||
"0 "+mem.Nick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "315", nick,
|
||||
[]string{channel}, "End of /WHO list",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
}
|
||||
|
||||
// handleWhoisCmd sends WHOIS reply numerics (311, 312,
|
||||
// 319, 318) for the target nick.
|
||||
func (hdlr *Handlers) handleWhoisCmd(
|
||||
request *http.Request,
|
||||
clientID, sessionID int64,
|
||||
nick, target string,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
|
||||
whoisNick := target
|
||||
if whoisNick == "" {
|
||||
lines := bodyLines()
|
||||
if len(lines) > 0 {
|
||||
whoisNick = strings.TrimSpace(lines[0])
|
||||
}
|
||||
}
|
||||
|
||||
if whoisNick == "" {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "461", nick,
|
||||
[]string{"WHOIS"}, "Not enough parameters",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
targetSID, err :=
|
||||
hdlr.params.Database.GetSessionByNick(
|
||||
ctx, whoisNick,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "401", nick,
|
||||
[]string{whoisNick},
|
||||
"No such nick/channel",
|
||||
)
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "318", nick,
|
||||
[]string{whoisNick},
|
||||
"End of /WHOIS list",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.sendWhoisNumerics(
|
||||
ctx, clientID, sessionID, nick,
|
||||
whoisNick, targetSID,
|
||||
)
|
||||
}
|
||||
|
||||
// sendWhoisNumerics emits 311/312/319/318 for a
|
||||
// resolved WHOIS target.
|
||||
func (hdlr *Handlers) sendWhoisNumerics(
|
||||
ctx context.Context,
|
||||
clientID, sessionID int64,
|
||||
nick, whoisNick string,
|
||||
targetSID int64,
|
||||
) {
|
||||
srvName := hdlr.serverName()
|
||||
|
||||
// 311 RPL_WHOISUSER
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "311", nick,
|
||||
[]string{whoisNick, whoisNick, "neoirc", "*"},
|
||||
whoisNick,
|
||||
)
|
||||
|
||||
// 312 RPL_WHOISSERVER
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "312", nick,
|
||||
[]string{whoisNick, srvName},
|
||||
srvName,
|
||||
)
|
||||
|
||||
// 319 RPL_WHOISCHANNELS
|
||||
channels, _ := hdlr.params.Database.GetSessionChannels(
|
||||
ctx, targetSID,
|
||||
)
|
||||
|
||||
if len(channels) > 0 {
|
||||
names := make([]string, 0, len(channels))
|
||||
|
||||
for _, chanInfo := range channels {
|
||||
names = append(names, chanInfo.Name)
|
||||
}
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "319", nick,
|
||||
[]string{whoisNick},
|
||||
strings.Join(names, " "),
|
||||
)
|
||||
}
|
||||
|
||||
// 318 RPL_ENDOFWHOIS
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "318", nick,
|
||||
[]string{whoisNick},
|
||||
"End of /WHOIS list",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) handleQuit(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
|
||||
Vendored
+2
-2
@@ -1,2 +1,2 @@
|
||||
var ie,w,Ue,pt,K,Ee,He,Re,De,he,de,pe,mt,Z={},Le=[],ht=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,ce=Array.isArray;function W(e,t){for(var n in t)e[n]=t[n];return e}function ye(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function p(e,t,n){var o,a,s,c={};for(s in t)s=="key"?o=t[s]:s=="ref"?a=t[s]:c[s]=t[s];if(arguments.length>2&&(c.children=arguments.length>3?ie.call(arguments,2):n),typeof e=="function"&&e.defaultProps!=null)for(s in e.defaultProps)c[s]===void 0&&(c[s]=e.defaultProps[s]);return oe(e,c,o,a,null)}function oe(e,t,n,o,a){var s={type:e,props:t,key:n,ref:o,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:a??++Ue,__i:-1,__u:0};return a==null&&w.vnode!=null&&w.vnode(s),s}function _e(e){return e.children}function se(e,t){this.props=e,this.context=t}function Y(e,t){if(t==null)return e.__?Y(e.__,e.__i+1):null;for(var n;t<e.__k.length;t++)if((n=e.__k[t])!=null&&n.__e!=null)return n.__e;return typeof e.type=="function"?Y(e):null}function Fe(e){var t,n;if((e=e.__)!=null&&e.__c!=null){for(e.__e=e.__c.base=null,t=0;t<e.__k.length;t++)if((n=e.__k[t])!=null&&n.__e!=null){e.__e=e.__c.base=n.__e;break}return Fe(e)}}function Me(e){(!e.__d&&(e.__d=!0)&&K.push(e)&&!ae.__r++||Ee!=w.debounceRendering)&&((Ee=w.debounceRendering)||He)(ae)}function ae(){for(var e,t,n,o,a,s,c,_=1;K.length;)K.length>_&&K.sort(Re),e=K.shift(),_=K.length,e.__d&&(n=void 0,o=void 0,a=(o=(t=e).__v).__e,s=[],c=[],t.__P&&((n=W({},o)).__v=o.__v+1,w.vnode&&w.vnode(n),ve(t.__P,n,o,t.__n,t.__P.namespaceURI,32&o.__u?[a]:null,s,a??Y(o),!!(32&o.__u),c),n.__v=o.__v,n.__.__k[n.__i]=n,Je(s,n,c),o.__e=o.__=null,n.__e!=a&&Fe(n)));ae.__r=0}function We(e,t,n,o,a,s,c,_,h,u,b){var i,m,y,C,M,I,k,g=o&&o.__k||Le,U=t.length;for(h=yt(n,t,g,h,U),i=0;i<U;i++)(y=n.__k[i])!=null&&(m=y.__i==-1?Z:g[y.__i]||Z,y.__i=i,I=ve(e,y,m,a,s,c,_,h,u,b),C=y.__e,y.ref&&m.ref!=y.ref&&(m.ref&&be(m.ref,null,y),b.push(y.ref,y.__c||C,y)),M==null&&C!=null&&(M=C),(k=!!(4&y.__u))||m.__k===y.__k?h=je(y,h,e,k):typeof y.type=="function"&&I!==void 0?h=I:C&&(h=C.nextSibling),y.__u&=-7);return n.__e=M,h}function yt(e,t,n,o,a){var s,c,_,h,u,b=n.length,i=b,m=0;for(e.__k=new Array(a),s=0;s<a;s++)(c=t[s])!=null&&typeof c!="boolean"&&typeof c!="function"?(typeof c=="string"||typeof c=="number"||typeof c=="bigint"||c.constructor==String?c=e.__k[s]=oe(null,c,null,null,null):ce(c)?c=e.__k[s]=oe(_e,{children:c},null,null,null):c.constructor===void 0&&c.__b>0?c=e.__k[s]=oe(c.type,c.props,c.key,c.ref?c.ref:null,c.__v):e.__k[s]=c,h=s+m,c.__=e,c.__b=e.__b+1,_=null,(u=c.__i=vt(c,n,h,i))!=-1&&(i--,(_=n[u])&&(_.__u|=2)),_==null||_.__v==null?(u==-1&&(a>b?m--:a<b&&m++),typeof c.type!="function"&&(c.__u|=4)):u!=h&&(u==h-1?m--:u==h+1?m++:(u>h?m--:m++,c.__u|=4))):e.__k[s]=null;if(i)for(s=0;s<b;s++)(_=n[s])!=null&&(2&_.__u)==0&&(_.__e==o&&(o=Y(_)),Be(_,_));return o}function je(e,t,n,o){var a,s;if(typeof e.type=="function"){for(a=e.__k,s=0;a&&s<a.length;s++)a[s]&&(a[s].__=e,t=je(a[s],t,n,o));return t}e.__e!=t&&(o&&(t&&e.type&&!t.parentNode&&(t=Y(e)),n.insertBefore(e.__e,t||null)),t=e.__e);do t=t&&t.nextSibling;while(t!=null&&t.nodeType==8);return t}function vt(e,t,n,o){var a,s,c,_=e.key,h=e.type,u=t[n],b=u!=null&&(2&u.__u)==0;if(u===null&&_==null||b&&_==u.key&&h==u.type)return n;if(o>(b?1:0)){for(a=n-1,s=n+1;a>=0||s<t.length;)if((u=t[c=a>=0?a--:s++])!=null&&(2&u.__u)==0&&_==u.key&&h==u.type)return c}return-1}function Oe(e,t,n){t[0]=="-"?e.setProperty(t,n??""):e[t]=n==null?"":typeof n!="number"||ht.test(t)?n:n+"px"}function re(e,t,n,o,a){var s,c;e:if(t=="style")if(typeof n=="string")e.style.cssText=n;else{if(typeof o=="string"&&(e.style.cssText=o=""),o)for(t in o)n&&t in n||Oe(e.style,t,"");if(n)for(t in n)o&&n[t]==o[t]||Oe(e.style,t,n[t])}else if(t[0]=="o"&&t[1]=="n")s=t!=(t=t.replace(De,"$1")),c=t.toLowerCase(),t=c in e||t=="onFocusOut"||t=="onFocusIn"?c.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+s]=n,n?o?n.u=o.u:(n.u=he,e.addEventListener(t,s?pe:de,s)):e.removeEventListener(t,s?pe:de,s);else{if(a=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=n??"";break e}catch{}typeof n=="function"||(n==null||n===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&n==1?"":n))}}function $e(e){return function(t){if(this.l){var n=this.l[t.type+e];if(t.t==null)t.t=he++;else if(t.t<n.u)return;return n(w.event?w.event(t):t)}}}function ve(e,t,n,o,a,s,c,_,h,u){var b,i,m,y,C,M,I,k,g,U,O,z,R,B,j,J,Q,P=t.type;if(t.constructor!==void 0)return null;128&n.__u&&(h=!!(32&n.__u),s=[_=t.__e=n.__e]),(b=w.__b)&&b(t);e:if(typeof P=="function")try{if(k=t.props,g="prototype"in P&&P.prototype.render,U=(b=P.contextType)&&o[b.__c],O=b?U?U.props.value:b.__:o,n.__c?I=(i=t.__c=n.__c).__=i.__E:(g?t.__c=i=new P(k,O):(t.__c=i=new se(k,O),i.constructor=P,i.render=kt),U&&U.sub(i),i.state||(i.state={}),i.__n=o,m=i.Line truncated
|
||||
`):"",d={id:r.id,ts:r.ts,from:r.from,to:r.to,command:r.command};switch(r.command){case"PRIVMSG":case"NOTICE":{let f=l,v=!1;Pt(f)&&(f=Nt(f),v=!0);let S={...d,text:f,system:!1,isAction:v},G=r.to;if(G&&G.startsWith("#"))D(G,S);else{let H=r.from===ne.current?r.to:r.from;s(fe=>fe.find(Ne=>Ne.type==="dm"&&Ne.name===H)?fe:[...fe,{type:"dm",name:H}]),D(H,S)}break}case"JOIN":{let f=`${r.from} has joined ${r.to}`;r.to&&D(r.to,{...d,text:f,system:!0}),r.to&&r.to.startsWith("#")&&q(r.to);break}case"PART":{let f=l?" ("+l+")":"",v=`${r.from} has parted ${r.to}${f}`;r.to&&D(r.to,{...d,text:v,system:!0}),r.to&&r.to.startsWith("#")&&q(r.to);break}case"QUIT":{let f=l?" ("+l+")":"",v=`${r.from} has quit${f}`;P.current.forEach(S=>{S.type==="channel"&&D(S.name,{...d,text:v,system:!0})});break}case"NICK":{let f=Array.isArray(r.body)?r.body[0]:l,v=`${r.from} is now known as ${f}`;P.current.forEach(S=>{S.type==="channel"&&D(S.name,{...d,text:v,system:!0})}),r.from===ne.current&&f&&o(f),P.current.forEach(S=>{S.type==="channel"&&q(S.name)});break}case"TOPIC":{let f=`${r.from} has changed the topic to: ${l}`;r.to&&(D(r.to,{...d,text:f,system:!0}),y(v=>({...v,[r.to]:l})));break}case"353":{if(Array.isArray(r.params)&&r.params.length>=2&&r.body){let f=r.params[1],G=(Array.isArray(r.body)?r.body[0]:String(r.body)).split(/\s+/).filter(Boolean).map(H=>H.startsWith("@")?{nick:H.slice(1),mode:"o"}:H.startsWith("+")?{nick:H.slice(1),mode:"v"}:{nick:H,mode:""});i(H=>({...H,[f]:G}))}break}case"332":{if(Array.isArray(r.params)&&r.params.length>=1){let f=r.params[0],v=Array.isArray(r.body)?r.body[0]:l;v&&y(S=>({...S,[f]:v}))}break}case"375":case"372":case"376":D("Server",{...d,text:l,system:!0});break;default:l&&D("Server",{...d,text:l,system:!0})}},[D,q]);$(()=>{if(!e)return;let r=!0;return(async()=>{for(;r;)try{let d=new AbortController;Q.current=d;let f=await E(`/messages?after=${j.current}&timeout=${Tt}`,{signal:d.signal});if(!r)break;if(U(!0),f.messages)for(let v of f.messages)ue(v);f.last_id>j.current&&(j.current=f.last_id)}catch(d){if(!r)break;if(d.name==="AbortError")continue;U(!1),await new Promise(f=>setTimeout(f,xt))}})(),()=>{r=!1,Q.current?.abort()}},[e,ue]),$(()=>{if(!e)return;let r=a[c];if(!r||r.type!=="channel")return;q(r.name);let l=setInterval(()=>q(r.name),It);return()=>clearInterval(l)},[e,c,a,q]),$(()=>{xe.current?.scrollIntoView({behavior:"smooth"})},[h,c]),$(()=>{Ie.current?.focus()},[c]),$(()=>{if(!e)return;let r=a[c];!r||r.type!=="channel"||E("/channels").then(l=>{let d=l.find(f=>f.name===r.name);d&&d.topic&&y(f=>({...f,[r.name]:d.topic}))}).catch(()=>{})},[e,c,a]);let st=X(async r=>{o(r),t(!0),T("Server",`Connected as ${r}`);let l=JSON.parse(localStorage.getItem("neoirc_channels")||"[]");for(let d of l)try{await E("/messages",{method:"POST",body:JSON.stringify({command:"JOIN",to:d})}),s(f=>f.find(v=>v.type==="channel"&&v.name===d)?f:[...f,{type:"channel",name:d}])}catch{}},[T]),at=async r=>{if(r){r=r.trim(),r.startsWith("#")||(r="#"+r);try{await E("/messages",{method:"POST",body:JSON.stringify({command:"JOIN",to:r})}),s(d=>d.find(f=>f.type==="channel"&&f.name===r)?d:[...d,{type:"channel",name:r}]);let l=P.current.length;_(l);try{let d=await E(`/history?target=${encodeURIComponent(r)}&limit=50`);if(Array.isArray(d))for(let f of d)ue(f)}catch{}}catch(l){T("Server",`Failed to join ${r}: ${l.data?.error||"error"}`)}}},Ae=async(r,l)=>{try{await E("/messages",{method:"POST",body:JSON.stringify(l?{command:"PART",to:r,body:[l]}:{command:"PART",to:r})})}catch{}s(d=>d.filter(f=>!(f.type==="channel"&&f.name===r))),_(0)},it=r=>{let l=a[r];l.type==="channel"?Ae(l.name):l.type==="dm"&&(s(d=>d.filter((f,v)=>v!==r)),c>=r&&_(Math.max(0,c-1)))},Pe=r=>{if(r===ne.current)return;s(d=>d.find(f=>f.type==="dm"&&f.name===r)?d:[...d,{type:"dm",name:r}]);let l=a.findIndex(d=>d.type==="dm"&&d.name===r);_(l>=0?l:a.length)},ct=async r=>{let l=r.split(" "),d=l[0].toLowerCase(),f=a[c];switch(d){case"/join":{l[1]?at(l[1]):T("Server","Usage: /join #channel");break}case"/part":{if(f.type==="channel"){let v=l.slice(1).join(" ")||void 0;Ae(f.name,v)}else T("Server","You are not in a channel");break}case"/msg":{if(l[1]&&l.slice(2).join(" ")){let v=l[1],S=l.slice(2).join(" ");try{await E("/messages",{method:"POST",body:JSON.stringify({command:"PRIVMSG",to:v,body:[S]})}),Pe(v)}catch(G){T("Server",`Message failed: ${G.data?.error||"error"}`)}}else T("Server","Usage: /msg <nick> <message>");break}case"/me":{if(f.type==="server"){T("Server","Cannot use /me in server window");break}let v=l.slice(1).join(" ");if(v)try{await E("/messages",{method:"POST",body:JSON.stringify({command:"PRIVMSG",to:f.name,body:[Ce+v+Te]})})}catch(S){T(f.name,`Action failed: ${S.data?.error||"error"}`)}else T("Server","Usage: /me <action>");break}case"/nick":{if(l[1])try{await E("/messages",{method:"POST",body:JSON.stringify({command:"NICK",body:[l[1]]})})}catch(v){T("Server",`Nick change failed: ${v.data?.error||"error"}`)}else T("Server","Usage: /nick <newnick>");break}case"/topic":{i
Line truncated
|
||||
var ie,T,He,pt,G,Pe,Ue,De,Re,ve,me,he,mt,ee={},Le=[],ht=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,_e=Array.isArray;function J(e,t){for(var n in t)e[n]=t[n];return e}function be(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function m(e,t,n){var o,s,a,_={};for(a in t)a=="key"?o=t[a]:a=="ref"?s=t[a]:_[a]=t[a];if(arguments.length>2&&(_.children=arguments.length>3?ie.call(arguments,2):n),typeof e=="function"&&e.defaultProps!=null)for(a in e.defaultProps)_[a]===void 0&&(_[a]=e.defaultProps[a]);return ae(e,_,o,s,null)}function ae(e,t,n,o,s){var a={type:e,props:t,key:n,ref:o,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:s??++He,__i:-1,__u:0};return s==null&&T.vnode!=null&&T.vnode(a),a}function le(e){return e.children}function se(e,t){this.props=e,this.context=t}function Y(e,t){if(t==null)return e.__?Y(e.__,e.__i+1):null;for(var n;t<e.__k.length;t++)if((n=e.__k[t])!=null&&n.__e!=null)return n.__e;return typeof e.type=="function"?Y(e):null}function We(e){var t,n;if((e=e.__)!=null&&e.__c!=null){for(e.__e=e.__c.base=null,t=0;t<e.__k.length;t++)if((n=e.__k[t])!=null&&n.__e!=null){e.__e=e.__c.base=n.__e;break}return We(e)}}function $e(e){(!e.__d&&(e.__d=!0)&&G.push(e)&&!ce.__r++||Pe!=T.debounceRendering)&&((Pe=T.debounceRendering)||Ue)(ce)}function ce(){for(var e,t,n,o,s,a,_,u=1;G.length;)G.length>u&&G.sort(De),e=G.shift(),u=G.length,e.__d&&(n=void 0,o=void 0,s=(o=(t=e).__v).__e,a=[],_=[],t.__P&&((n=J({},o)).__v=o.__v+1,T.vnode&&T.vnode(n),ke(t.__P,n,o,t.__n,t.__P.namespaceURI,32&o.__u?[s]:null,a,s??Y(o),!!(32&o.__u),_),n.__v=o.__v,n.__.__k[n.__i]=n,je(a,n,_),o.__e=o.__=null,n.__e!=s&&We(n)));ce.__r=0}function Fe(e,t,n,o,s,a,_,u,y,f,b){var c,h,v,C,E,A,g,w=o&&o.__k||Le,U=t.length;for(y=yt(n,t,w,y,U),c=0;c<U;c++)(v=n.__k[c])!=null&&(h=v.__i==-1?ee:w[v.__i]||ee,v.__i=c,A=ke(e,v,h,s,a,_,u,y,f,b),C=v.__e,v.ref&&h.ref!=v.ref&&(h.ref&&ge(h.ref,null,v),b.push(v.ref,v.__c||C,v)),E==null&&C!=null&&(E=C),(g=!!(4&v.__u))||h.__k===v.__k?y=Je(v,y,e,g):typeof v.type=="function"&&A!==void 0?y=A:C&&(y=C.nextSibling),v.__u&=-7);return n.__e=E,y}function yt(e,t,n,o,s){var a,_,u,y,f,b=n.length,c=b,h=0;for(e.__k=new Array(s),a=0;a<s;a++)(_=t[a])!=null&&typeof _!="boolean"&&typeof _!="function"?(typeof _=="string"||typeof _=="number"||typeof _=="bigint"||_.constructor==String?_=e.__k[a]=ae(null,_,null,null,null):_e(_)?_=e.__k[a]=ae(le,{children:_},null,null,null):_.constructor===void 0&&_.__b>0?_=e.__k[a]=ae(_.type,_.props,_.key,_.ref?_.ref:null,_.__v):e.__k[a]=_,y=a+h,_.__=e,_.__b=e.__b+1,u=null,(f=_.__i=vt(_,n,y,c))!=-1&&(c--,(u=n[f])&&(u.__u|=2)),u==null||u.__v==null?(f==-1&&(s>b?h--:s<b&&h++),typeof _.type!="function"&&(_.__u|=4)):f!=y&&(f==y-1?h--:f==y+1?h++:(f>y?h--:h++,_.__u|=4))):e.__k[a]=null;if(c)for(a=0;a<b;a++)(u=n[a])!=null&&(2&u.__u)==0&&(u.__e==o&&(o=Y(u)),qe(u,u));return o}function Je(e,t,n,o){var s,a;if(typeof e.type=="function"){for(s=e.__k,a=0;s&&a<s.length;a++)s[a]&&(s[a].__=e,t=Je(s[a],t,n,o));return t}e.__e!=t&&(o&&(t&&e.type&&!t.parentNode&&(t=Y(e)),n.insertBefore(e.__e,t||null)),t=e.__e);do t=t&&t.nextSibling;while(t!=null&&t.nodeType==8);return t}function vt(e,t,n,o){var s,a,_,u=e.key,y=e.type,f=t[n],b=f!=null&&(2&f.__u)==0;if(f===null&&u==null||b&&u==f.key&&y==f.type)return n;if(o>(b?1:0)){for(s=n-1,a=n+1;s>=0||a<t.length;)if((f=t[_=s>=0?s--:a++])!=null&&(2&f.__u)==0&&u==f.key&&y==f.type)return _}return-1}function Ee(e,t,n){t[0]=="-"?e.setProperty(t,n??""):e[t]=n==null?"":typeof n!="number"||ht.test(t)?n:n+"px"}function oe(e,t,n,o,s){var a,_;e:if(t=="style")if(typeof n=="string")e.style.cssText=n;else{if(typeof o=="string"&&(e.style.cssText=o=""),o)for(t in o)n&&t in n||Ee(e.style,t,"");if(n)for(t in n)o&&n[t]==o[t]||Ee(e.style,t,n[t])}else if(t[0]=="o"&&t[1]=="n")a=t!=(t=t.replace(Re,"$1")),_=t.toLowerCase(),t=_ in e||t=="onFocusOut"||t=="onFocusIn"?_.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+a]=n,n?o?n.u=o.u:(n.u=ve,e.addEventListener(t,a?he:me,a)):e.removeEventListener(t,a?he:me,a);else{if(s=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=n??"";break e}catch{}typeof n=="function"||(n==null||n===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&n==1?"":n))}}function Me(e){return function(t){if(this.l){var n=this.l[t.type+e];if(t.t==null)t.t=ve++;else if(t.t<n.u)return;return n(T.event?T.event(t):t)}}}function ke(e,t,n,o,s,a,_,u,y,f){var b,c,h,v,C,E,A,g,w,U,M,z,R,B,j,V,Q,P=t.type;if(t.constructor!==void 0)return null;128&n.__u&&(y=!!(32&n.__u),a=[u=t.__e=n.__e]),(b=T.__b)&&b(t);e:if(typeof P=="function")try{if(g=t.props,w="prototype"in P&&P.prototype.render,U=(b=P.contextType)&&o[b.__c],M=b?U?U.props.value:b.__:o,n.__c?A=(c=t.__c=n.__c).__=c.__E:(w?t.__c=c=new P(g,M):(t.__c=c=new se(g,M),c.constructor=P,c.render=kt),U&&U.sub(c),c.state||(c.state={}),c.__n=o,hLine truncated
|
||||
`):"",d={id:r.id,ts:r.ts,from:r.from,to:r.to,command:r.command};switch(r.command){case"PRIVMSG":case"NOTICE":{let l=i,p=!1;Ot(l)&&(l=Nt(l),p=!0);let k={...d,text:l,system:!1,isAction:p},F=r.to;if(F&&F.startsWith("#"))N(F,k);else{let D=r.from===re.current?r.to:r.from;a(pe=>pe.find(Ne=>Ne.type==="dm"&&Ne.name===D)?pe:[...pe,{type:"dm",name:D}]),N(D,k)}break}case"JOIN":{let l=`${r.from} has joined ${r.to}`;r.to&&N(r.to,{...d,text:l,system:!0}),r.to&&r.to.startsWith("#")&&K(r.to);break}case"PART":{let l=i?" ("+i+")":"",p=`${r.from} has parted ${r.to}${l}`;r.to&&N(r.to,{...d,text:p,system:!0}),r.to&&r.to.startsWith("#")&&K(r.to);break}case"QUIT":{let l=i?" ("+i+")":"",p=`${r.from} has quit${l}`;P.current.forEach(k=>{k.type==="channel"&&N(k.name,{...d,text:p,system:!0})});break}case"NICK":{let l=Array.isArray(r.body)?r.body[0]:i,p=`${r.from} is now known as ${l}`;P.current.forEach(k=>{k.type==="channel"&&N(k.name,{...d,text:p,system:!0})}),r.from===re.current&&l&&o(l),P.current.forEach(k=>{k.type==="channel"&&K(k.name)});break}case"TOPIC":{let l=`${r.from} has changed the topic to: ${i}`;r.to&&(N(r.to,{...d,text:l,system:!0}),v(p=>({...p,[r.to]:i})));break}case"353":{if(Array.isArray(r.params)&&r.params.length>=2&&r.body){let l=r.params[1],F=(Array.isArray(r.body)?r.body[0]:String(r.body)).split(/\s+/).filter(Boolean).map(D=>D.startsWith("@")?{nick:D.slice(1),mode:"o"}:D.startsWith("+")?{nick:D.slice(1),mode:"v"}:{nick:D,mode:""});c(D=>({...D,[l]:F}))}break}case"332":{if(Array.isArray(r.params)&&r.params.length>=1){let l=r.params[0],p=Array.isArray(r.body)?r.body[0]:i;p&&v(k=>({...k,[l]:p}))}break}case"322":{if(Array.isArray(r.params)&&r.params.length>=2){let l=r.params[0],p=r.params[1];N("Server",{...d,text:`${l} (${p} users): ${(i||"").trim()}`,system:!0})}break}case"323":N("Server",{...d,text:i||"End of channel list",system:!0});break;case"352":{if(Array.isArray(r.params)&&r.params.length>=5){let l=r.params[0],p=r.params[4],k=r.params.length>5?r.params[5]:"";N("Server",{...d,text:`${l} ${p} ${k}`,system:!0})}break}case"315":N("Server",{...d,text:i||"End of /WHO list",system:!0});break;case"311":{if(Array.isArray(r.params)&&r.params.length>=1){let l=r.params[0];N("Server",{...d,text:`${l} (${i})`,system:!0})}break}case"312":{if(Array.isArray(r.params)&&r.params.length>=2){let l=r.params[0],p=r.params[1];N("Server",{...d,text:`${l} on ${p}`,system:!0})}break}case"319":{if(Array.isArray(r.params)&&r.params.length>=1){let l=r.params[0];N("Server",{...d,text:`${l} is on: ${i}`,system:!0})}break}case"318":N("Server",{...d,text:i||"End of /WHOIS list",system:!0});break;case"375":case"372":case"376":N("Server",{...d,text:i,system:!0});break;default:i&&N("Server",{...d,text:i,system:!0})}},[N,K]);H(()=>{if(!e)return;let r=!0;return(async()=>{for(;r;)try{let d=new AbortController;Q.current=d;let l=await x(`/messages?after=${j.current}&timeout=${xt}`,{signal:d.signal});if(!r)break;if(U(!0),l.messages)for(let p of l.messages)fe(p);l.last_id>j.current&&(j.current=l.last_id)}catch(d){if(!r)break;if(d.name==="AbortError")continue;U(!1),await new Promise(l=>setTimeout(l,Ct))}})(),()=>{r=!1,Q.current?.abort()}},[e,fe]),H(()=>{if(!e)return;let r=s[_];if(!r||r.type!=="channel")return;K(r.name);let i=setInterval(()=>K(r.name),It);return()=>clearInterval(i)},[e,_,s,K]),H(()=>{Ae.current?.scrollIntoView({behavior:"smooth"})},[y,_]),H(()=>{Z.current?.focus()},[_]),H(()=>{let r=i=>{i.key==="/"&&document.activeElement!==Z.current&&!i.ctrlKey&&!i.altKey&&!i.metaKey&&(i.preventDefault(),Z.current?.focus())};return document.addEventListener("keydown",r),Z.current?.focus(),()=>document.removeEventListener("keydown",r)},[]),H(()=>{if(!e)return;let r=s[_];!r||r.type!=="channel"||x("/channels").then(i=>{let d=i.find(l=>l.name===r.name);d&&d.topic&&v(l=>({...l,[r.name]:d.topic}))}).catch(()=>{})},[e,_,s]);let at=X(async(r,i)=>{if(o(r),t(!0),S("Server",`Connected as ${r}`),i)try{await x("/messages",{method:"POST",body:JSON.stringify({command:"MOTD"})})}catch{}let d=JSON.parse(localStorage.getItem("neoirc_channels")||"[]");for(let l of d)try{await x("/messages",{method:"POST",body:JSON.stringify({command:"JOIN",to:l})}),a(p=>p.find(k=>k.type==="channel"&&k.name===l)?p:[...p,{type:"channel",name:l}])}catch{}},[S]),st=async r=>{if(r){r=r.trim(),r.startsWith("#")||(r="#"+r);try{await x("/messages",{method:"POST",body:JSON.stringify({command:"JOIN",to:r})}),a(d=>d.find(l=>l.type==="channel"&&l.name===r)?d:[...d,{type:"channel",name:r}]);let i=P.current.length;u(i);try{let d=await x(`/history?target=${encodeURIComponent(r)}&limit=50`);if(Array.isArray(d))for(let l of d)fe(l)}catch{}}catch(i){S("Server",`Failed to join ${r}: ${i.data?.error||"error"}`)}}},Oe=async(r,i)=>{try{await x("/messages",{method:"POST",body:JSON.stringify(i?{command:"PART",to:r,body:[i]}:{command:"PART",to:r})})}catch{}a(d=>d.filter(l=>!(l.type==="channel"&&l.name===r))),u(0)},ct=r=>{let i=s[r];i.type==="channel"?Oe(i.name):i.type==="dm"&&(a(d=>d.filter((l,p)=>p!==r)),_>=r&&u
Line truncated
|
||||
Vendored
+5
-5
@@ -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 {
|
||||
|
||||
+244
-2
@@ -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();
|
||||
@@ -419,6 +419,100 @@ function App() {
|
||||
|
||||
break;
|
||||
}
|
||||
case "322": {
|
||||
// RPL_LIST — channel, member count, topic.
|
||||
if (Array.isArray(msg.params) && msg.params.length >= 2) {
|
||||
const chName = msg.params[0];
|
||||
const count = msg.params[1];
|
||||
const chTopic = body || "";
|
||||
addMessage("Server", {
|
||||
...base,
|
||||
text: `${chName} (${count} users): ${chTopic.trim()}`,
|
||||
system: true,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "323":
|
||||
addMessage("Server", {
|
||||
...base,
|
||||
text: body || "End of channel list",
|
||||
system: true,
|
||||
});
|
||||
|
||||
break;
|
||||
case "352": {
|
||||
// RPL_WHOREPLY — channel, user, host, server, nick, flags.
|
||||
if (Array.isArray(msg.params) && msg.params.length >= 5) {
|
||||
const whoCh = msg.params[0];
|
||||
const whoNick = msg.params[4];
|
||||
const whoFlags = msg.params.length > 5 ? msg.params[5] : "";
|
||||
addMessage("Server", {
|
||||
...base,
|
||||
text: `${whoCh} ${whoNick} ${whoFlags}`,
|
||||
system: true,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "315":
|
||||
addMessage("Server", {
|
||||
...base,
|
||||
text: body || "End of /WHO list",
|
||||
system: true,
|
||||
});
|
||||
|
||||
break;
|
||||
case "311": {
|
||||
// RPL_WHOISUSER — nick, user, host, *, realname.
|
||||
if (Array.isArray(msg.params) && msg.params.length >= 1) {
|
||||
const wiNick = msg.params[0];
|
||||
addMessage("Server", {
|
||||
...base,
|
||||
text: `${wiNick} (${body})`,
|
||||
system: true,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "312": {
|
||||
// RPL_WHOISSERVER — nick, server, server info.
|
||||
if (Array.isArray(msg.params) && msg.params.length >= 2) {
|
||||
const wiNick = msg.params[0];
|
||||
const wiServer = msg.params[1];
|
||||
addMessage("Server", {
|
||||
...base,
|
||||
text: `${wiNick} on ${wiServer}`,
|
||||
system: true,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "319": {
|
||||
// RPL_WHOISCHANNELS — nick, channels.
|
||||
if (Array.isArray(msg.params) && msg.params.length >= 1) {
|
||||
const wiNick = msg.params[0];
|
||||
addMessage("Server", {
|
||||
...base,
|
||||
text: `${wiNick} is on: ${body}`,
|
||||
system: true,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "318":
|
||||
addMessage("Server", {
|
||||
...base,
|
||||
text: body || "End of /WHOIS list",
|
||||
system: true,
|
||||
});
|
||||
|
||||
break;
|
||||
case "375":
|
||||
case "372":
|
||||
case "376":
|
||||
@@ -497,6 +591,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 +631,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,16 +924,125 @@ 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 "/list": {
|
||||
try {
|
||||
await api("/messages", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ command: "LIST" }),
|
||||
});
|
||||
} catch (err) {
|
||||
addSystemMessage(
|
||||
"Server",
|
||||
`Failed to list channels: ${err.data?.error || "error"}`,
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "/who": {
|
||||
const whoTarget = parts[1] || (tab.type === "channel" ? tab.name : "");
|
||||
if (whoTarget) {
|
||||
try {
|
||||
await api("/messages", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ command: "WHO", to: whoTarget }),
|
||||
});
|
||||
} catch (err) {
|
||||
addSystemMessage(
|
||||
"Server",
|
||||
`WHO failed: ${err.data?.error || "error"}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
addSystemMessage("Server", "Usage: /who #channel");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "/whois": {
|
||||
if (parts[1]) {
|
||||
try {
|
||||
await api("/messages", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ command: "WHOIS", to: parts[1] }),
|
||||
});
|
||||
} catch (err) {
|
||||
addSystemMessage(
|
||||
"Server",
|
||||
`WHOIS failed: ${err.data?.error || "error"}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
addSystemMessage("Server", "Usage: /whois <nick>");
|
||||
}
|
||||
|
||||
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",
|
||||
" /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",
|
||||
" /list — List all channels",
|
||||
" /who [#channel] — List users in a channel",
|
||||
" /whois nick — Show info about a user",
|
||||
" /clear — Clear messages in the current tab",
|
||||
" /quit [reason] — Disconnect from server",
|
||||
" /help — Show this help",
|
||||
];
|
||||
|
||||
+5
-5
@@ -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