commit b813210

delthas  ·  2021-11-24 15:59:00 +0000 UTC
parent 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, -104
M app.go
M app.go
+110, -76
  1@@ -642,21 +642,9 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
  2 			Highlight: true,
  3 		})
  4 	case irc.UserNickEvent:
  5-		var body ui.StyledStringBuilder
  6-		body.WriteString(fmt.Sprintf("%s\u2192%s", ev.FormerNick, ev.User))
  7-		textStyle := tcell.StyleDefault.Foreground(tcell.ColorGray)
  8-		arrowStyle := tcell.StyleDefault
  9-		body.AddStyle(0, textStyle)
 10-		body.AddStyle(len(ev.FormerNick), arrowStyle)
 11-		body.AddStyle(body.Len()-len(ev.User), textStyle)
 12+		line := app.formatEvent(ev)
 13 		for _, c := range s.ChannelsSharedWith(ev.User) {
 14-			app.win.AddLine(netID, c, ui.NotifyNone, ui.Line{
 15-				At:        msg.TimeOrNow(),
 16-				Head:      "--",
 17-				HeadColor: tcell.ColorGray,
 18-				Body:      body.StyledString(),
 19-				Mergeable: true,
 20-			})
 21+			app.win.AddLine(netID, c, ui.NotifyNone, line)
 22 		}
 23 	case irc.SelfJoinEvent:
 24 		i, added := app.win.AddBuffer(netID, "", ev.Channel)
 25@@ -685,70 +673,27 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
 26 			app.lastBuffer = ""
 27 		}
 28 	case irc.UserJoinEvent:
 29-		var body ui.StyledStringBuilder
 30-		body.Grow(len(ev.User) + 1)
 31-		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGreen))
 32-		body.WriteByte('+')
 33-		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
 34-		body.WriteString(ev.User)
 35-		app.win.AddLine(netID, ev.Channel, ui.NotifyNone, ui.Line{
 36-			At:        msg.TimeOrNow(),
 37-			Head:      "--",
 38-			HeadColor: tcell.ColorGray,
 39-			Body:      body.StyledString(),
 40-			Mergeable: true,
 41-		})
 42+		line := app.formatEvent(ev)
 43+		app.win.AddLine(netID, ev.Channel, ui.NotifyNone, line)
 44 	case irc.SelfPartEvent:
 45 		app.win.RemoveBuffer(netID, ev.Channel)
 46 		delete(app.messageBounds, boundKey{netID, ev.Channel})
 47 	case irc.UserPartEvent:
 48-		var body ui.StyledStringBuilder
 49-		body.Grow(len(ev.User) + 1)
 50-		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorRed))
 51-		body.WriteByte('-')
 52-		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
 53-		body.WriteString(ev.User)
 54-		app.win.AddLine(netID, ev.Channel, ui.NotifyNone, ui.Line{
 55-			At:        msg.TimeOrNow(),
 56-			Head:      "--",
 57-			HeadColor: tcell.ColorGray,
 58-			Body:      body.StyledString(),
 59-			Mergeable: true,
 60-		})
 61+		line := app.formatEvent(ev)
 62+		app.win.AddLine(netID, ev.Channel, ui.NotifyNone, line)
 63 	case irc.UserQuitEvent:
 64-		var body ui.StyledStringBuilder
 65-		body.Grow(len(ev.User) + 1)
 66-		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorRed))
 67-		body.WriteByte('-')
 68-		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
 69-		body.WriteString(ev.User)
 70+		line := app.formatEvent(ev)
 71 		for _, c := range ev.Channels {
 72-			app.win.AddLine(netID, c, ui.NotifyNone, ui.Line{
 73-				At:        msg.TimeOrNow(),
 74-				Head:      "--",
 75-				HeadColor: tcell.ColorGray,
 76-				Body:      body.StyledString(),
 77-				Mergeable: true,
 78-			})
 79+			app.win.AddLine(netID, c, ui.NotifyNone, line)
 80 		}
 81 	case irc.TopicChangeEvent:
 82+		line := app.formatEvent(ev)
 83+		app.win.AddLine(netID, ev.Channel, ui.NotifyUnread, line)
 84 		topic := ui.IRCString(ev.Topic).String()
 85-		body := fmt.Sprintf("Topic changed to: %s", topic)
 86 		app.win.SetTopic(netID, ev.Channel, topic)
 87-		app.win.AddLine(netID, ev.Channel, ui.NotifyUnread, ui.Line{
 88-			At:        msg.TimeOrNow(),
 89-			Head:      "--",
 90-			HeadColor: tcell.ColorGray,
 91-			Body:      ui.Styled(body, tcell.StyleDefault.Foreground(tcell.ColorGray)),
 92-		})
 93 	case irc.ModeChangeEvent:
 94-		body := fmt.Sprintf("Mode change: %s", ev.Mode)
 95-		app.win.AddLine(netID, ev.Channel, ui.NotifyUnread, ui.Line{
 96-			At:        msg.TimeOrNow(),
 97-			Head:      "--",
 98-			HeadColor: tcell.ColorGray,
 99-			Body:      ui.Styled(body, tcell.StyleDefault.Foreground(tcell.ColorGray)),
100-		})
101+		line := app.formatEvent(ev)
102+		app.win.AddLine(netID, ev.Channel, ui.NotifyUnread, line)
103 	case irc.InviteEvent:
104 		var buffer string
105 		var notify ui.NotifyType
106@@ -807,19 +752,25 @@ func (app *App) handleIRCEvent(netID string, ev interface{}) {
107 		var linesAfter []ui.Line
108 		bounds, hasBounds := app.messageBounds[boundKey{netID, ev.Target}]
109 		for _, m := range ev.Messages {
110+			var line ui.Line
111 			switch ev := m.(type) {
112 			case irc.MessageEvent:
113-				_, line, _ := app.formatMessage(s, ev)
114-				if hasBounds {
115-					c := bounds.Compare(&line)
116-					if c < 0 {
117-						linesBefore = append(linesBefore, line)
118-					} else if c > 0 {
119-						linesAfter = append(linesAfter, line)
120-					}
121-				} else {
122+				_, line, _ = app.formatMessage(s, ev)
123+			default:
124+				line = app.formatEvent(ev)
125+			}
126+			if line.IsZero() {
127+				continue
128+			}
129+			if hasBounds {
130+				c := bounds.Compare(&line)
131+				if c < 0 {
132 					linesBefore = append(linesBefore, line)
133+				} else if c > 0 {
134+					linesAfter = append(linesAfter, line)
135 				}
136+			} else {
137+				linesBefore = append(linesBefore, line)
138 			}
139 		}
140 		app.win.AddLines(netID, ev.Target, linesBefore, linesAfter)
141@@ -987,6 +938,89 @@ func (app *App) completions(cursorIdx int, text []rune) []ui.Completion {
142 	return cs
143 }
144 
145+// formatEvent returns a formatted ui.Line for an irc.Event.
146+func (app *App) formatEvent(ev irc.Event) ui.Line {
147+	switch ev := ev.(type) {
148+	case irc.UserNickEvent:
149+		var body ui.StyledStringBuilder
150+		body.WriteString(fmt.Sprintf("%s\u2192%s", ev.FormerNick, ev.User))
151+		textStyle := tcell.StyleDefault.Foreground(tcell.ColorGray)
152+		arrowStyle := tcell.StyleDefault
153+		body.AddStyle(0, textStyle)
154+		body.AddStyle(len(ev.FormerNick), arrowStyle)
155+		body.AddStyle(body.Len()-len(ev.User), textStyle)
156+
157+		return ui.Line{
158+			At:        ev.Time,
159+			Head:      "--",
160+			HeadColor: tcell.ColorGray,
161+			Body:      body.StyledString(),
162+			Mergeable: true,
163+		}
164+	case irc.UserJoinEvent:
165+		var body ui.StyledStringBuilder
166+		body.Grow(len(ev.User) + 1)
167+		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGreen))
168+		body.WriteByte('+')
169+		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
170+		body.WriteString(ev.User)
171+		return ui.Line{
172+			At:        ev.Time,
173+			Head:      "--",
174+			HeadColor: tcell.ColorGray,
175+			Body:      body.StyledString(),
176+			Mergeable: true,
177+		}
178+	case irc.UserPartEvent:
179+		var body ui.StyledStringBuilder
180+		body.Grow(len(ev.User) + 1)
181+		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorRed))
182+		body.WriteByte('-')
183+		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
184+		body.WriteString(ev.User)
185+		return ui.Line{
186+			At:        ev.Time,
187+			Head:      "--",
188+			HeadColor: tcell.ColorGray,
189+			Body:      body.StyledString(),
190+			Mergeable: true,
191+		}
192+	case irc.UserQuitEvent:
193+		var body ui.StyledStringBuilder
194+		body.Grow(len(ev.User) + 1)
195+		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorRed))
196+		body.WriteByte('-')
197+		body.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorGray))
198+		body.WriteString(ev.User)
199+		return ui.Line{
200+			At:        ev.Time,
201+			Head:      "--",
202+			HeadColor: tcell.ColorGray,
203+			Body:      body.StyledString(),
204+			Mergeable: true,
205+		}
206+	case irc.TopicChangeEvent:
207+		topic := ui.IRCString(ev.Topic).String()
208+		body := fmt.Sprintf("Topic changed to: %s", topic)
209+		return ui.Line{
210+			At:        ev.Time,
211+			Head:      "--",
212+			HeadColor: tcell.ColorGray,
213+			Body:      ui.Styled(body, tcell.StyleDefault.Foreground(tcell.ColorGray)),
214+		}
215+	case irc.ModeChangeEvent:
216+		body := fmt.Sprintf("Mode change: %s", ev.Mode)
217+		return ui.Line{
218+			At:        ev.Time,
219+			Head:      "--",
220+			HeadColor: tcell.ColorGray,
221+			Body:      ui.Styled(body, tcell.StyleDefault.Foreground(tcell.ColorGray)),
222+		}
223+	default:
224+		return ui.Line{}
225+	}
226+}
227+
228 // formatMessage sets how a given message must be formatted.
229 //
230 // It computes three things:
+6, -0
 1@@ -19,6 +19,7 @@ type SelfNickEvent struct {
 2 type UserNickEvent struct {
 3 	User       string
 4 	FormerNick string
 5+	Time       time.Time
 6 }
 7 
 8 type SelfJoinEvent struct {
 9@@ -30,6 +31,7 @@ type SelfJoinEvent struct {
10 type UserJoinEvent struct {
11 	User    string
12 	Channel string
13+	Time    time.Time
14 }
15 
16 type SelfPartEvent struct {
17@@ -39,21 +41,25 @@ type SelfPartEvent struct {
18 type UserPartEvent struct {
19 	User    string
20 	Channel string
21+	Time    time.Time
22 }
23 
24 type UserQuitEvent struct {
25 	User     string
26 	Channels []string
27+	Time     time.Time
28 }
29 
30 type TopicChangeEvent struct {
31 	Channel string
32 	Topic   string
33+	Time    time.Time
34 }
35 
36 type ModeChangeEvent struct {
37 	Channel string
38 	Mode    string
39+	Time    time.Time
40 }
41 
42 type InviteEvent struct {
+82, -6
  1@@ -58,6 +58,7 @@ var SupportedCapabilities = map[string]struct{}{
  2 	"setname":       {},
  3 
  4 	"draft/chathistory":        {},
  5+	"draft/event-playback":     {},
  6 	"soju.im/bouncer-networks": {},
  7 }
  8 
  9@@ -527,18 +528,23 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
 10 			}
 11 			s.targetsBatch.Targets[target] = t
 12 		} else if b, ok := s.chBatches[id]; ok {
 13-			ev, err := s.newMessageEvent(msg)
 14+			ev, err := s.handleMessageRegistered(msg, true)
 15 			if err != nil {
 16 				return nil, err
 17 			}
 18-			s.chBatches[id] = HistoryEvent{
 19-				Target:   b.Target,
 20-				Messages: append(b.Messages, ev),
 21+			if ev != nil {
 22+				s.chBatches[id] = HistoryEvent{
 23+					Target:   b.Target,
 24+					Messages: append(b.Messages, ev),
 25+				}
 26+				return nil, nil
 27 			}
 28-			return nil, nil
 29 		}
 30 	}
 31+	return s.handleMessageRegistered(msg, false)
 32+}
 33 
 34+func (s *Session) handleMessageRegistered(msg Message, playback bool) (Event, error) {
 35 	switch msg.Command {
 36 	case "AUTHENTICATE":
 37 		if s.auth == nil {
 38@@ -663,6 +669,14 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
 39 			return nil, err
 40 		}
 41 
 42+		if playback {
 43+			return UserJoinEvent{
 44+				User:    msg.Prefix.Name,
 45+				Channel: channel,
 46+				Time:    msg.TimeOrNow(),
 47+			}, nil
 48+		}
 49+
 50 		nickCf := s.Casemap(msg.Prefix.Name)
 51 		channelCf := s.Casemap(channel)
 52 
 53@@ -685,6 +699,7 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
 54 			return UserJoinEvent{
 55 				User:    msg.Prefix.Name,
 56 				Channel: c.Name,
 57+				Time:    msg.TimeOrNow(),
 58 			}, nil
 59 		}
 60 	case "PART":
 61@@ -697,6 +712,14 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
 62 			return nil, err
 63 		}
 64 
 65+		if playback {
 66+			return UserPartEvent{
 67+				User:    msg.Prefix.Name,
 68+				Channel: channel,
 69+				Time:    msg.TimeOrNow(),
 70+			}, nil
 71+		}
 72+
 73 		nickCf := s.Casemap(msg.Prefix.Name)
 74 		channelCf := s.Casemap(channel)
 75 
 76@@ -718,6 +741,7 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
 77 				return UserPartEvent{
 78 					User:    u.Name.Name,
 79 					Channel: c.Name,
 80+					Time:    msg.TimeOrNow(),
 81 				}, nil
 82 			}
 83 		}
 84@@ -727,6 +751,14 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
 85 			return nil, err
 86 		}
 87 
 88+		if playback {
 89+			return UserPartEvent{
 90+				User:    nick,
 91+				Channel: channel,
 92+				Time:    msg.TimeOrNow(),
 93+			}, nil
 94+		}
 95+
 96 		nickCf := s.Casemap(nick)
 97 		channelCf := s.Casemap(channel)
 98 
 99@@ -748,6 +780,7 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
100 				return UserPartEvent{
101 					User:    nick,
102 					Channel: c.Name,
103+					Time:    msg.TimeOrNow(),
104 				}, nil
105 			}
106 		}
107@@ -756,6 +789,13 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
108 			return nil, errMissingPrefix
109 		}
110 
111+		if playback {
112+			return UserQuitEvent{
113+				User: msg.Prefix.Name,
114+				Time: msg.TimeOrNow(),
115+			}, nil
116+		}
117+
118 		nickCf := s.Casemap(msg.Prefix.Name)
119 
120 		if u, ok := s.users[nickCf]; ok {
121@@ -771,6 +811,7 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
122 			return UserQuitEvent{
123 				User:     u.Name.Name,
124 				Channels: channels,
125+				Time:     msg.TimeOrNow(),
126 			}, nil
127 		}
128 	case rplNamreply:
129@@ -864,6 +905,14 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
130 			return nil, err
131 		}
132 
133+		if playback {
134+			return TopicChangeEvent{
135+				Channel: channel,
136+				Topic:   topic,
137+				Time:    msg.TimeOrNow(),
138+			}, nil
139+		}
140+
141 		channelCf := s.Casemap(channel)
142 
143 		if c, ok := s.channels[channelCf]; ok {
144@@ -874,6 +923,7 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
145 			return TopicChangeEvent{
146 				Channel: c.Name,
147 				Topic:   c.Topic,
148+				Time:    msg.TimeOrNow(),
149 			}, nil
150 		}
151 	case "MODE":
152@@ -882,6 +932,14 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
153 			return nil, err
154 		}
155 
156+		if playback {
157+			return ModeChangeEvent{
158+				Channel: channel,
159+				Mode:    mode,
160+				Time:    msg.TimeOrNow(),
161+			}, nil
162+		}
163+
164 		channelCf := s.Casemap(channel)
165 
166 		if c, ok := s.channels[channelCf]; ok {
167@@ -917,7 +975,8 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
168 			s.channels[channelCf] = c
169 			return ModeChangeEvent{
170 				Channel: c.Name,
171-				Mode:    strings.Join(msg.Params[1:], " "),
172+				Mode:    mode,
173+				Time:    msg.TimeOrNow(),
174 			}, nil
175 		}
176 	case "INVITE":
177@@ -966,12 +1025,20 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
178 			return nil, err
179 		}
180 
181+		if playback {
182+			return s.newMessageEvent(msg)
183+		}
184+
185 		targetCf := s.casemap(target)
186 		nickCf := s.casemap(msg.Prefix.Name)
187 		s.typings.Done(targetCf, nickCf)
188 
189 		return s.newMessageEvent(msg)
190 	case "TAGMSG":
191+		if playback {
192+			return nil, nil
193+		}
194+
195 		if msg.Prefix == nil {
196 			return nil, errMissingPrefix
197 		}
198@@ -1048,6 +1115,14 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
199 			return nil, err
200 		}
201 
202+		if playback {
203+			return UserNickEvent{
204+				User:       nick,
205+				FormerNick: msg.Prefix.Name,
206+				Time:       msg.TimeOrNow(),
207+			}, nil
208+		}
209+
210 		nickCf := s.Casemap(msg.Prefix.Name)
211 		newNick := nick
212 		newNickCf := s.Casemap(newNick)
213@@ -1070,6 +1145,7 @@ func (s *Session) handleRegistered(msg Message) (Event, error) {
214 			return UserNickEvent{
215 				User:       nick,
216 				FormerNick: msg.Prefix.Name,
217+				Time:       msg.TimeOrNow(),
218 			}, nil
219 		}
220 	case "BOUNCER":
+31, -22
 1@@ -39,6 +39,21 @@ type Line struct {
 2 	newLines    []int
 3 }
 4 
 5+func (l *Line) IsZero() bool {
 6+	return l.Body.string == ""
 7+}
 8+
 9+func (l *Line) Merge(line Line) {
10+	newBody := new(StyledStringBuilder)
11+	newBody.Grow(len(l.Body.string) + 2 + len(line.Body.string))
12+	newBody.WriteStyledString(l.Body)
13+	newBody.WriteString("  ")
14+	newBody.WriteStyledString(line.Body)
15+	l.Body = newBody.StyledString()
16+	l.computeSplitPoints()
17+	l.width = 0
18+}
19+
20 func (l *Line) computeSplitPoints() {
21 	if l.splitPoints == nil {
22 		l.splitPoints = []point{}
23@@ -307,14 +322,7 @@ func (bs *BufferList) AddLine(netID, title string, notify NotifyType, line Line)
24 
25 	if line.Mergeable && n != 0 && b.lines[n-1].Mergeable {
26 		l := &b.lines[n-1]
27-		newBody := new(StyledStringBuilder)
28-		newBody.Grow(len(l.Body.string) + 2 + len(line.Body.string))
29-		newBody.WriteStyledString(l.Body)
30-		newBody.WriteString("  ")
31-		newBody.WriteStyledString(line.Body)
32-		l.Body = newBody.StyledString()
33-		l.computeSplitPoints()
34-		l.width = 0
35+		l.Merge(line)
36 		// TODO change b.scrollAmt if it's not 0 and bs.current is idx.
37 	} else {
38 		line.computeSplitPoints()
39@@ -340,21 +348,22 @@ func (bs *BufferList) AddLines(netID, title string, before, after []Line) {
40 
41 	b := &bs.list[idx]
42 
43-	for i := 0; i < len(before); i++ {
44-		before[i].Body = before[i].Body.ParseURLs()
45-		before[i].computeSplitPoints()
46-	}
47-	for i := 0; i < len(after); i++ {
48-		after[i].Body = after[i].Body.ParseURLs()
49-		after[i].computeSplitPoints()
50-	}
51-
52-	if len(before) != 0 {
53-		b.lines = append(before, b.lines...)
54-	}
55-	if len(after) != 0 {
56-		b.lines = append(b.lines, after...)
57+	lines := make([]Line, 0, len(before)+len(b.lines)+len(after))
58+	for _, buf := range []*[]Line{&before, &b.lines, &after} {
59+		for _, line := range *buf {
60+			if line.Mergeable && len(lines) > 0 && lines[len(lines)-1].Mergeable {
61+				l := &lines[len(lines)-1]
62+				l.Merge(line)
63+			} else {
64+				if buf != &b.lines {
65+					line.Body = line.Body.ParseURLs()
66+					line.computeSplitPoints()
67+				}
68+				lines = append(lines, line)
69+			}
70+		}
71 	}
72+	b.lines = lines
73 }
74 
75 func (bs *BufferList) SetTopic(netID, title string, topic string) {