cchat-gtk/internal/ui/messages/typing/state.go

146 lines
2.8 KiB
Go

package typing
import (
"sort"
"time"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/pkg/errors"
)
type State struct {
// states
typers []cchat.Typer
timeout time.Duration
canceler func()
invalidated bool
// consts
changed func(s *State, empty bool)
stopper func() // stops the event loop, not used atm
}
var _ cchat.TypingContainer = (*State)(nil)
func NewState(changed func(s *State, empty bool)) *State {
s := &State{changed: changed}
s.stopper = gts.AfterFunc(time.Second/2, s.loop)
return s
}
func (s *State) reset() {
if s.canceler != nil {
s.canceler()
s.canceler = nil
}
s.timeout = 0
s.typers = nil
s.invalidated = false
}
// Subscribe is thread-safe.
func (s *State) Subscribe(indicator cchat.TypingIndicator) {
gts.Async(func() (func(), error) {
c, err := indicator.TypingSubscribe(s)
if err != nil {
return nil, errors.Wrap(err, "Failed to subscribe to typing indicator")
}
return func() {
s.canceler = c
s.timeout = indicator.TypingTimeout()
}, nil
})
}
// loop runs a single iteration of the event loop. This function is not
// thread-safe.
func (s *State) loop() {
// Filter out any expired typers.
t, ok := filterTypers(s.typers, s.timeout)
if ok {
s.invalidated = true
s.typers = t
}
// Call the event handler if things are invalidated.
if s.invalidated {
s.changed(s, len(s.typers) == 0)
s.invalidated = false
}
}
// invalidate sorts and invalidates the state.
func (s *State) invalidate() {
// Sort the list of typers again.
sort.Slice(s.typers, func(i, j int) bool {
return s.typers[i].Time().Before(s.typers[j].Time())
})
s.invalidated = true
}
// AddTyper is thread-safe.
func (s *State) AddTyper(typer cchat.Typer) {
gts.ExecAsync(func() {
defer s.invalidate()
// If the typer already exists, then pop them to the start of the list.
for i, t := range s.typers {
if t.ID() == typer.ID() {
s.typers[i] = t
return
}
}
s.typers = append(s.typers, typer)
})
}
// RemoveTyper is thread-safe.
func (s *State) RemoveTyper(typerID string) {
gts.ExecAsync(func() { s.removeTyper(typerID) })
}
func (s *State) removeTyper(typerID string) {
defer s.invalidate()
for i, t := range s.typers {
if t.ID() == typerID {
// Remove the quick way. Sort will take care of ordering.
l := len(s.typers) - 1
s.typers[i] = s.typers[l]
s.typers[l] = nil
s.typers = s.typers[:l]
return
}
}
}
func filterTypers(typers []cchat.Typer, timeout time.Duration) ([]cchat.Typer, bool) {
// Fast path.
if len(typers) == 0 || timeout == 0 {
return nil, false
}
var now = time.Now()
var cut int
for _, t := range typers {
if now.Sub(t.Time()) < timeout {
typers[cut] = t
cut++
}
}
for i := cut; i < len(typers); i++ {
typers[i] = nil
}
var changed = cut != len(typers)
return typers[:cut], changed
}