diff --git a/internal/cli/app.go b/internal/cli/app.go index 4bacf80..eae6d63 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -96,20 +96,7 @@ func (a *App) handleInput(text string) { a.ui.AddStatus( "[red]Send error: " + err.Error(), ) - - return } - - timestamp := time.Now().Format(timeFormat) - - a.mu.Lock() - nick := a.nick - a.mu.Unlock() - - a.ui.AddLine(target, fmt.Sprintf( - "[gray]%s [green]<%s>[white] %s", - timestamp, nick, text, - )) } func (a *App) handleCommand(text string) { @@ -301,9 +288,6 @@ func (a *App) cmdJoin(channel string) { a.mu.Unlock() a.ui.SwitchToBuffer(channel) - a.ui.AddLine(channel, - "[yellow]*** Joined "+channel, - ) a.ui.SetStatus(nick, channel, "connected") } @@ -338,10 +322,6 @@ func (a *App) cmdPart(channel string) { return } - a.ui.AddLine(channel, - "[yellow]*** Left "+channel, - ) - a.mu.Lock() if a.target == channel { a.target = "" @@ -368,7 +348,6 @@ func (a *App) cmdMsg(args string) { a.mu.Lock() connected := a.connected - nick := a.nick a.mu.Unlock() if !connected { @@ -386,16 +365,7 @@ func (a *App) cmdMsg(args string) { a.ui.AddStatus(fmt.Sprintf( "[red]Send failed: %v", err, )) - - return } - - timestamp := time.Now().Format(timeFormat) - - a.ui.AddLine(target, fmt.Sprintf( - "[gray]%s [green]<%s>[white] %s", - timestamp, nick, text, - )) } func (a *App) cmdQuery(nick string) { @@ -785,12 +755,10 @@ func (a *App) handlePrivmsgEvent( lines := msg.BodyLines() text := strings.Join(lines, " ") - if msg.From == myNick { - return - } - + // A direct message shows in the buffer of the other user: + // the recipient of one the user sent, else the sender. target := msg.To - if !strings.HasPrefix(target, "#") { + if !strings.HasPrefix(target, "#") && msg.From != myNick { target = msg.From } @@ -813,31 +781,35 @@ func (a *App) handleJoinEvent( )) } -// handlePartEvent shows another user's part. The server -// also sends the user's own part back to it, which cmdPart -// has already shown; showing it again would add a second -// line and mark the channel just left as unread. +// handlePartEvent shows a part in the channel's buffer. The +// user's own part does not mark the channel unread: the user +// has just left it. func (a *App) handlePartEvent( msg *api.Message, timestamp, myNick string, ) { - if msg.To == "" || msg.From == myNick { + if msg.To == "" { return } lines := msg.BodyLines() reason := strings.Join(lines, " ") + line := fmt.Sprintf( + "[gray]%s [yellow]*** %s has left %s", + timestamp, msg.From, msg.To, + ) + if reason != "" { - a.ui.AddLine(msg.To, fmt.Sprintf( - "[gray]%s [yellow]*** %s has left %s (%s)", - timestamp, msg.From, msg.To, reason, - )) - } else { - a.ui.AddLine(msg.To, fmt.Sprintf( - "[gray]%s [yellow]*** %s has left %s", - timestamp, msg.From, msg.To, - )) + line += " (" + reason + ")" } + + if msg.From == myNick { + a.ui.AddLineAsRead(msg.To, line) + + return + } + + a.ui.AddLine(msg.To, line) } func (a *App) handleQuitEvent( diff --git a/internal/cli/app_internal_test.go b/internal/cli/app_internal_test.go index 9aa41d7..0048740 100644 --- a/internal/cli/app_internal_test.go +++ b/internal/cli/app_internal_test.go @@ -1,6 +1,8 @@ package cli import ( + "net/http" + "net/http/httptest" "testing" "github.com/gdamore/tcell/v2" @@ -53,19 +55,18 @@ func TestMemberPrefix(t *testing.T) { } // TestPartEvent checks that a PART event adds a line to the -// channel's buffer and marks it unread, except for the -// user's own part, which cmdPart has already shown. +// channel's buffer and marks it unread, except that the +// user's own part does not mark it unread. func TestPartEvent(t *testing.T) { t.Parallel() tests := []struct { name string from string - wantLines int wantUnread int }{ - {name: "own part", from: "erin", wantLines: 0, wantUnread: 0}, - {name: "other member", from: "bob", wantLines: 1, wantUnread: 1}, + {name: "own part", from: "erin", wantUnread: 0}, + {name: "other member", from: "bob", wantUnread: 1}, } for _, test := range tests { @@ -79,22 +80,165 @@ func TestPartEvent(t *testing.T) { Command: irc.CmdPart, From: test.from, To: "#chan", }) - var lines, unread int - - app.ui.app.QueueUpdate(func() { - buf := app.ui.getOrCreateBuffer("#chan") - lines = len(buf.Lines) - unread = buf.Unread - }) - - if lines != test.wantLines || unread != test.wantUnread { - t.Fatalf("got %d lines and %d unread, want %d and %d", - lines, unread, test.wantLines, test.wantUnread) + lines, unread := bufferState(app.ui, "#chan") + if lines != 1 || unread != test.wantUnread { + t.Fatalf("got %d lines and %d unread, want 1 and %d", + lines, unread, test.wantUnread) } }) } } +// TestOwnCommandShownOnce checks that a message, join or +// part made from neoirc-cli shows as one line in the +// channel's buffer once the server's event for it arrives, +// and does not mark the channel unread. +func TestOwnCommandShownOnce(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + send func(app *App) + event api.Message + }{ + { + name: "message", + send: func(app *App) { app.handleInput("hello") }, + event: api.Message{ + Command: irc.CmdPrivmsg, From: "erin", To: "#chan", + Body: []string{"hello"}, + }, + }, + { + name: "join", + send: func(app *App) { app.cmdJoin("#chan") }, + event: api.Message{ + Command: irc.CmdJoin, From: "erin", To: "#chan", + }, + }, + { + name: "part", + send: func(app *App) { app.cmdPart("#chan") }, + event: api.Message{ + Command: irc.CmdPart, From: "erin", To: "#chan", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + // The server accepts every command and answers nothing. + server := httptest.NewServer(http.HandlerFunc( + func(http.ResponseWriter, *http.Request) {}, + )) + t.Cleanup(server.Close) + + app := &App{ + ui: NewUI(), + client: api.NewClient(server.URL), + nick: "erin", + target: "#chan", + connected: true, + } + runUI(t, app.ui) + app.ui.SwitchToBuffer("#chan") + + test.send(app) + app.handleServerMessage(&test.event) + + lines, unread := bufferState(app.ui, "#chan") + if lines != 1 || unread != 0 { + t.Fatalf("got %d lines and %d unread, want 1 and 0", + lines, unread) + } + }) + } +} + +// TestOwnEventFromOtherClient checks that a message, notice, +// join or part that the user's session makes from another +// client shows as one line in the buffer it belongs to. +func TestOwnEventFromOtherClient(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + event api.Message + buffer string + }{ + { + name: "channel message", + event: api.Message{ + Command: irc.CmdPrivmsg, From: "erin", To: "#chan", + Body: []string{"hello"}, + }, + buffer: "#chan", + }, + { + name: "direct message", + event: api.Message{ + Command: irc.CmdPrivmsg, From: "erin", To: "bob", + Body: []string{"hello"}, + }, + buffer: "bob", + }, + { + name: "notice", + event: api.Message{ + Command: irc.CmdNotice, From: "erin", To: "#chan", + Body: []string{"hello"}, + }, + buffer: "(status)", + }, + { + name: "join", + event: api.Message{ + Command: irc.CmdJoin, From: "erin", To: "#chan", + }, + buffer: "#chan", + }, + { + name: "part", + event: api.Message{ + Command: irc.CmdPart, From: "erin", To: "#chan", + }, + buffer: "#chan", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + app := &App{ui: NewUI(), nick: "erin"} + runUI(t, app.ui) + + app.handleServerMessage(&test.event) + + lines, _ := bufferState(app.ui, test.buffer) + if lines != 1 { + t.Fatalf("got %d lines in %s, want 1", lines, test.buffer) + } + }) + } +} + +// bufferState returns the number of lines in the named +// buffer and how many of them are unread. +func bufferState(ui *UI, name string) (int, int) { + var lines, unread int + + ui.app.QueueUpdate(func() { + buf := ui.getOrCreateBuffer(name) + lines = len(buf.Lines) + unread = buf.Unread + }) + + return lines, unread +} + // runUI runs ui on a simulated screen until the test ends. func runUI(t *testing.T, ui *UI) { t.Helper() diff --git a/internal/cli/ui.go b/internal/cli/ui.go index 80f5fc9..b36d714 100644 --- a/internal/cli/ui.go +++ b/internal/cli/ui.go @@ -95,6 +95,19 @@ func (ui *UI) AddLine(bufferName, line string) { }) } +// AddLineAsRead adds a line to the specified buffer, as +// AddLine does, but does not mark the buffer unread. +func (ui *UI) AddLineAsRead(bufferName, line string) { + ui.app.QueueUpdateDraw(func() { + buf := ui.getOrCreateBuffer(bufferName) + buf.Lines = append(buf.Lines, line) + + if ui.buffers[ui.currentBuffer] == buf { + _, _ = fmt.Fprintln(ui.messages, line) + } + }) +} + // AddStatus adds a line to the status buffer. func (ui *UI) AddStatus(line string) { ts := time.Now().Format("15:04")