~taiite/senpai

b813210eb6b7a50607985a14f969e33914ae0956 — delthas 2 months ago d41d7c5
Add support for draft/event-playback

- Refactor formatting lines into a function
- Store event times in the event
- Refactor merging lines into a function
- Always merge lines from the history, now that we will add mergeable
  lines with CHATHISTORY

Successfully tested locally.
4 files changed, 229 insertions(+), 104 deletions(-)

M app.go
M irc/events.go
M irc/session.go
M ui/buffers.go
M app.go => app.go +110 -76
@@ 642,21 642,9 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
			Highlight: true,
		})
	case irc.UserNickEvent:
		var body ui.StyledStringBuilder
		body.WriteString(fmt.Sprintf("%s\u2192%s", ev.FormerNick, ev.User))
		textStyle := tcell.StyleDefault.Foreground(tcell.ColorGray)
		arrowStyle := tcell.StyleDefault
		body.AddStyle(0, textStyle)
		body.AddStyle(len(ev.FormerNick), arrowStyle)
		body.AddStyle(body.Len()-len(ev.User), textStyle)
		line := app.formatEvent(ev)
		for _, c := range s.ChannelsSharedWith(ev.User) {
			app.win.AddLine(netID, c, ui.NotifyNone, ui.Line{
				At:        msg.TimeOrNow(),
				Head:      "--",
				HeadColor: tcell.ColorGray,
				Body:      body.StyledString(),
				Mergeable: true,
			})
			app.win.AddLine(netID, c, ui.NotifyNone, line)
		}
	case irc.SelfJoinEvent:
		i, added := app.win.AddBuffer(netID, "", ev.Channel)


@@ 685,70 673,27 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
			app.lastBuffer = ""
		}
	case irc.UserJoinEvent:
		var body ui.StyledStringBuilder
		body.Grow(len(ev.User) + 1)
		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGreen))
		body.WriteByte('+')
		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
		body.WriteString(ev.User)
		app.win.AddLine(netID, ev.Channel, ui.NotifyNone, ui.Line{
			At:        msg.TimeOrNow(),
			Head:      "--",
			HeadColor: tcell.ColorGray,
			Body:      body.StyledString(),
			Mergeable: true,
		})
		line := app.formatEvent(ev)
		app.win.AddLine(netID, ev.Channel, ui.NotifyNone, line)
	case irc.SelfPartEvent:
		app.win.RemoveBuffer(netID, ev.Channel)
		delete(app.messageBounds, boundKey{netID, ev.Channel})
	case irc.UserPartEvent:
		var body ui.StyledStringBuilder
		body.Grow(len(ev.User) + 1)
		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorRed))
		body.WriteByte('-')
		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
		body.WriteString(ev.User)
		app.win.AddLine(netID, ev.Channel, ui.NotifyNone, ui.Line{
			At:        msg.TimeOrNow(),
			Head:      "--",
			HeadColor: tcell.ColorGray,
			Body:      body.StyledString(),
			Mergeable: true,
		})
		line := app.formatEvent(ev)
		app.win.AddLine(netID, ev.Channel, ui.NotifyNone, line)
	case irc.UserQuitEvent:
		var body ui.StyledStringBuilder
		body.Grow(len(ev.User) + 1)
		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorRed))
		body.WriteByte('-')
		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
		body.WriteString(ev.User)
		line := app.formatEvent(ev)
		for _, c := range ev.Channels {
			app.win.AddLine(netID, c, ui.NotifyNone, ui.Line{
				At:        msg.TimeOrNow(),
				Head:      "--",
				HeadColor: tcell.ColorGray,
				Body:      body.StyledString(),
				Mergeable: true,
			})
			app.win.AddLine(netID, c, ui.NotifyNone, line)
		}
	case irc.TopicChangeEvent:
		line := app.formatEvent(ev)
		app.win.AddLine(netID, ev.Channel, ui.NotifyUnread, line)
		topic := ui.IRCString(ev.Topic).String()
		body := fmt.Sprintf("Topic changed to: %s", topic)
		app.win.SetTopic(netID, ev.Channel, topic)
		app.win.AddLine(netID, ev.Channel, ui.NotifyUnread, ui.Line{
			At:        msg.TimeOrNow(),
			Head:      "--",
			HeadColor: tcell.ColorGray,
			Body:      ui.Styled(body, tcell.StyleDefault.Foreground(tcell.ColorGray)),
		})
	case irc.ModeChangeEvent:
		body := fmt.Sprintf("Mode change: %s", ev.Mode)
		app.win.AddLine(netID, ev.Channel, ui.NotifyUnread, ui.Line{
			At:        msg.TimeOrNow(),
			Head:      "--",
			HeadColor: tcell.ColorGray,
			Body:      ui.Styled(body, tcell.StyleDefault.Foreground(tcell.ColorGray)),
		})
		line := app.formatEvent(ev)
		app.win.AddLine(netID, ev.Channel, ui.NotifyUnread, line)
	case irc.InviteEvent:
		var buffer string
		var notify ui.NotifyType


@@ 807,19 752,25 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
		var linesAfter []ui.Line
		bounds, hasBounds := app.messageBounds[boundKey{netID, ev.Target}]
		for _, m := range ev.Messages {
			var line ui.Line
			switch ev := m.(type) {
			case irc.MessageEvent:
				_, line, _ := app.formatMessage(s, ev)
				if hasBounds {
					c := bounds.Compare(&line)
					if c < 0 {
						linesBefore = append(linesBefore, line)
					} else if c > 0 {
						linesAfter = append(linesAfter, line)
					}
				} else {
				_, line, _ = app.formatMessage(s, ev)
			default:
				line = app.formatEvent(ev)
			}
			if line.IsZero() {
				continue
			}
			if hasBounds {
				c := bounds.Compare(&line)
				if c < 0 {
					linesBefore = append(linesBefore, line)
				} else if c > 0 {
					linesAfter = append(linesAfter, line)
				}
			} else {
				linesBefore = append(linesBefore, line)
			}
		}
		app.win.AddLines(netID, ev.Target, linesBefore, linesAfter)


@@ 987,6 938,89 @@ func (app *App) completions(cursorIdx int, text []rune) []ui.Completion {
	return cs
}

// formatEvent returns a formatted ui.Line for an irc.Event.
func (app *App) formatEvent(ev irc.Event) ui.Line {
	switch ev := ev.(type) {
	case irc.UserNickEvent:
		var body ui.StyledStringBuilder
		body.WriteString(fmt.Sprintf("%s\u2192%s", ev.FormerNick, ev.User))
		textStyle := tcell.StyleDefault.Foreground(tcell.ColorGray)
		arrowStyle := tcell.StyleDefault
		body.AddStyle(0, textStyle)
		body.AddStyle(len(ev.FormerNick), arrowStyle)
		body.AddStyle(body.Len()-len(ev.User), textStyle)

		return ui.Line{
			At:        ev.Time,
			Head:      "--",
			HeadColor: tcell.ColorGray,
			Body:      body.StyledString(),
			Mergeable: true,
		}
	case irc.UserJoinEvent:
		var body ui.StyledStringBuilder
		body.Grow(len(ev.User) + 1)
		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGreen))
		body.WriteByte('+')
		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
		body.WriteString(ev.User)
		return ui.Line{
			At:        ev.Time,
			Head:      "--",
			HeadColor: tcell.ColorGray,
			Body:      body.StyledString(),
			Mergeable: true,
		}
	case irc.UserPartEvent:
		var body ui.StyledStringBuilder
		body.Grow(len(ev.User) + 1)
		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorRed))
		body.WriteByte('-')
		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
		body.WriteString(ev.User)
		return ui.Line{
			At:        ev.Time,
			Head:      "--",
			HeadColor: tcell.ColorGray,
			Body:      body.StyledString(),
			Mergeable: true,
		}
	case irc.UserQuitEvent:
		var body ui.StyledStringBuilder
		body.Grow(len(ev.User) + 1)
		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorRed))
		body.WriteByte('-')
		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
		body.WriteString(ev.User)
		return ui.Line{
			At:        ev.Time,
			Head:      "--",
			HeadColor: tcell.ColorGray,
			Body:      body.StyledString(),
			Mergeable: true,
		}
	case irc.TopicChangeEvent:
		topic := ui.IRCString(ev.Topic).String()
		body := fmt.Sprintf("Topic changed to: %s", topic)
		return ui.Line{
			At:        ev.Time,
			Head:      "--",
			HeadColor: tcell.ColorGray,
			Body:      ui.Styled(body, tcell.StyleDefault.Foreground(tcell.ColorGray)),
		}
	case irc.ModeChangeEvent:
		body := fmt.Sprintf("Mode change: %s", ev.Mode)
		return ui.Line{
			At:        ev.Time,
			Head:      "--",
			HeadColor: tcell.ColorGray,
			Body:      ui.Styled(body, tcell.StyleDefault.Foreground(tcell.ColorGray)),
		}
	default:
		return ui.Line{}
	}
}

// formatMessage sets how a given message must be formatted.
//
// It computes three things:

M irc/events.go => irc/events.go +6 -0
@@ 19,6 19,7 @@ type SelfNickEvent struct {
type UserNickEvent struct {
	User       string
	FormerNick string
	Time       time.Time
}

type SelfJoinEvent struct {


@@ 30,6 31,7 @@ type SelfJoinEvent struct {
type UserJoinEvent struct {
	User    string
	Channel string
	Time    time.Time
}

type SelfPartEvent struct {


@@ 39,21 41,25 @@ type SelfPartEvent struct {
type UserPartEvent struct {
	User    string
	Channel string
	Time    time.Time
}

type UserQuitEvent struct {
	User     string
	Channels []string
	Time     time.Time
}

type TopicChangeEvent struct {
	Channel string
	Topic   string
	Time    time.Time
}

type ModeChangeEvent struct {
	Channel string
	Mode    string
	Time    time.Time
}

type InviteEvent struct {

M irc/session.go => irc/session.go +82 -6
@@ 58,6 58,7 @@ var SupportedCapabilities = map[string]struct{}{
	"setname":       {},

	"draft/chathistory":        {},
	"draft/event-playback":     {},
	"soju.im/bouncer-networks": {},
}



@@ 527,18 528,23 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
			}
			s.targetsBatch.Targets[target] = t
		} else if b, ok := s.chBatches[id]; ok {
			ev, err := s.newMessageEvent(msg)
			ev, err := s.handleMessageRegistered(msg, true)
			if err != nil {
				return nil, err
			}
			s.chBatches[id] = HistoryEvent{
				Target:   b.Target,
				Messages: append(b.Messages, ev),
			if ev != nil {
				s.chBatches[id] = HistoryEvent{
					Target:   b.Target,
					Messages: append(b.Messages, ev),
				}
				return nil, nil
			}
			return nil, nil
		}
	}
	return s.handleMessageRegistered(msg, false)
}

func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, error) {
	switch msg.Command {
	case "AUTHENTICATE":
		if s.auth == nil {


@@ 663,6 669,14 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
			return nil, err
		}

		if playback {
			return UserJoinEvent{
				User:    msg.Prefix.Name,
				Channel: channel,
				Time:    msg.TimeOrNow(),
			}, nil
		}

		nickCf := s.Casemap(msg.Prefix.Name)
		channelCf := s.Casemap(channel)



@@ 685,6 699,7 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
			return UserJoinEvent{
				User:    msg.Prefix.Name,
				Channel: c.Name,
				Time:    msg.TimeOrNow(),
			}, nil
		}
	case "PART":


@@ 697,6 712,14 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
			return nil, err
		}

		if playback {
			return UserPartEvent{
				User:    msg.Prefix.Name,
				Channel: channel,
				Time:    msg.TimeOrNow(),
			}, nil
		}

		nickCf := s.Casemap(msg.Prefix.Name)
		channelCf := s.Casemap(channel)



@@ 718,6 741,7 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
				return UserPartEvent{
					User:    u.Name.Name,
					Channel: c.Name,
					Time:    msg.TimeOrNow(),
				}, nil
			}
		}


@@ 727,6 751,14 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
			return nil, err
		}

		if playback {
			return UserPartEvent{
				User:    nick,
				Channel: channel,
				Time:    msg.TimeOrNow(),
			}, nil
		}

		nickCf := s.Casemap(nick)
		channelCf := s.Casemap(channel)



@@ 748,6 780,7 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
				return UserPartEvent{
					User:    nick,
					Channel: c.Name,
					Time:    msg.TimeOrNow(),
				}, nil
			}
		}


@@ 756,6 789,13 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
			return nil, errMissingPrefix
		}

		if playback {
			return UserQuitEvent{
				User: msg.Prefix.Name,
				Time: msg.TimeOrNow(),
			}, nil
		}

		nickCf := s.Casemap(msg.Prefix.Name)

		if u, ok := s.users[nickCf]; ok {


@@ 771,6 811,7 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
			return UserQuitEvent{
				User:     u.Name.Name,
				Channels: channels,
				Time:     msg.TimeOrNow(),
			}, nil
		}
	case rplNamreply:


@@ 864,6 905,14 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
			return nil, err
		}

		if playback {
			return TopicChangeEvent{
				Channel: channel,
				Topic:   topic,
				Time:    msg.TimeOrNow(),
			}, nil
		}

		channelCf := s.Casemap(channel)

		if c, ok := s.channels[channelCf]; ok {


@@ 874,6 923,7 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
			return TopicChangeEvent{
				Channel: c.Name,
				Topic:   c.Topic,
				Time:    msg.TimeOrNow(),
			}, nil
		}
	case "MODE":


@@ 882,6 932,14 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
			return nil, err
		}

		if playback {
			return ModeChangeEvent{
				Channel: channel,
				Mode:    mode,
				Time:    msg.TimeOrNow(),
			}, nil
		}

		channelCf := s.Casemap(channel)

		if c, ok := s.channels[channelCf]; ok {


@@ 917,7 975,8 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
			s.channels[channelCf] = c
			return ModeChangeEvent{
				Channel: c.Name,
				Mode:    strings.Join(msg.Params[1:], " "),
				Mode:    mode,
				Time:    msg.TimeOrNow(),
			}, nil
		}
	case "INVITE":


@@ 966,12 1025,20 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
			return nil, err
		}

		if playback {
			return s.newMessageEvent(msg)
		}

		targetCf := s.casemap(target)
		nickCf := s.casemap(msg.Prefix.Name)
		s.typings.Done(targetCf, nickCf)

		return s.newMessageEvent(msg)
	case "TAGMSG":
		if playback {
			return nil, nil
		}

		if msg.Prefix == nil {
			return nil, errMissingPrefix
		}


@@ 1048,6 1115,14 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
			return nil, err
		}

		if playback {
			return UserNickEvent{
				User:       nick,
				FormerNick: msg.Prefix.Name,
				Time:       msg.TimeOrNow(),
			}, nil
		}

		nickCf := s.Casemap(msg.Prefix.Name)
		newNick := nick
		newNickCf := s.Casemap(newNick)


@@ 1070,6 1145,7 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
			return UserNickEvent{
				User:       nick,
				FormerNick: msg.Prefix.Name,
				Time:       msg.TimeOrNow(),
			}, nil
		}
	case "BOUNCER":

M ui/buffers.go => ui/buffers.go +31 -22
@@ 39,6 39,21 @@ type Line struct {
	newLines    []int
}

func (l *Line) IsZero() bool {
	return l.Body.string == ""
}

func (l *Line) Merge(line Line) {
	newBody := new(StyledStringBuilder)
	newBody.Grow(len(l.Body.string) + 2 + len(line.Body.string))
	newBody.WriteStyledString(l.Body)
	newBody.WriteString("  ")
	newBody.WriteStyledString(line.Body)
	l.Body = newBody.StyledString()
	l.computeSplitPoints()
	l.width = 0
}

func (l *Line) computeSplitPoints() {
	if l.splitPoints == nil {
		l.splitPoints = []point{}


@@ 307,14 322,7 @@ func (bs *BufferList) AddLine(netID, title string, notify NotifyType, line Line)

	if line.Mergeable && n != 0 && b.lines[n-1].Mergeable {
		l := &b.lines[n-1]
		newBody := new(StyledStringBuilder)
		newBody.Grow(len(l.Body.string) + 2 + len(line.Body.string))
		newBody.WriteStyledString(l.Body)
		newBody.WriteString("  ")
		newBody.WriteStyledString(line.Body)
		l.Body = newBody.StyledString()
		l.computeSplitPoints()
		l.width = 0
		l.Merge(line)
		// TODO change b.scrollAmt if it's not 0 and bs.current is idx.
	} else {
		line.computeSplitPoints()


@@ 340,21 348,22 @@ func (bs *BufferList) AddLines(netID, title string, before, after []Line) {

	b := &bs.list[idx]

	for i := 0; i < len(before); i++ {
		before[i].Body = before[i].Body.ParseURLs()
		before[i].computeSplitPoints()
	}
	for i := 0; i < len(after); i++ {
		after[i].Body = after[i].Body.ParseURLs()
		after[i].computeSplitPoints()
	}

	if len(before) != 0 {
		b.lines = append(before, b.lines...)
	}
	if len(after) != 0 {
		b.lines = append(b.lines, after...)
	lines := make([]Line, 0, len(before)+len(b.lines)+len(after))
	for _, buf := range []*[]Line{&before, &b.lines, &after} {
		for _, line := range *buf {
			if line.Mergeable && len(lines) > 0 && lines[len(lines)-1].Mergeable {
				l := &lines[len(lines)-1]
				l.Merge(line)
			} else {
				if buf != &b.lines {
					line.Body = line.Body.ParseURLs()
					line.computeSplitPoints()
				}
				lines = append(lines, line)
			}
		}
	}
	b.lines = lines
}

func (bs *BufferList) SetTopic(netID, title string, topic string) {