mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2024-11-16 19:22:51 +00:00
179 lines
3.3 KiB
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
|
|
}
|