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

179 lines
3.3 KiB
Go

package typing
import (
"context"
"sort"
"time"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
"github.com/pkg/errors"
)
type typer struct {
cchat.User
s *rich.NameContainer
t time.Time
}
type State struct {
// states
typers []typer
timeout time.Duration
canceler func()
invalidated bool
// consts
changed func(s *State, empty bool)
stopper func()
}
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.update()
s.invalidated = false
}
}
// update force-runs th callback.
func (s *State) update() {
s.changed(s, len(s.typers) == 0)
}
// 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].t.Before(s.typers[j].t)
})
s.invalidated = true
}
// AddTyper is thread-safe.
func (s *State) AddTyper(user cchat.User) {
now := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), s.timeout)
state := rich.NameContainer{}
state.QueueNamer(ctx, user)
gts.ExecAsync(func() {
defer cancel()
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() == user.ID() {
s.typers[i] = t
return
}
}
state.OnUpdate(s.update)
s.typers = append(s.typers, typer{
User: user,
s: &state,
t: now,
})
})
}
// 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 {
continue
}
// Invalidate the typer's label state.
t.s.Stop()
// Remove the quick way. Sort will take care of ordering.
l := len(s.typers) - 1
s.typers[i] = s.typers[l]
s.typers[l] = typer{}
s.typers = s.typers[:l]
return
}
}
func filterTypers(typers []typer, timeout time.Duration) ([]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.t) < timeout {
typers[cut] = t
cut++
}
}
for i := cut; i < len(typers); i++ {
typers[i].s.Stop()
typers[i] = typer{}
}
var changed = cut != len(typers)
return typers[:cut], changed
}