github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/typingmonitor.go (about)

     1  package chat
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  	"time"
     8  
     9  	"strings"
    10  
    11  	"github.com/keybase/client/go/chat/globals"
    12  	"github.com/keybase/client/go/chat/utils"
    13  	"github.com/keybase/client/go/protocol/chat1"
    14  	"github.com/keybase/clockwork"
    15  )
    16  
    17  const typingTimeout = 10 * time.Second
    18  const maxExtensions = 50
    19  
    20  type typingControlChans struct {
    21  	typer chat1.TyperInfo
    22  
    23  	stopCh   chan struct{}
    24  	extendCh chan struct{}
    25  }
    26  
    27  func newTypingControlChans(typer chat1.TyperInfo) *typingControlChans {
    28  	return &typingControlChans{
    29  		typer: typer,
    30  		// Might not need these buffers, but we really don't want to deadlock
    31  		stopCh:   make(chan struct{}, 5),
    32  		extendCh: make(chan struct{}, 5),
    33  	}
    34  }
    35  
    36  type TypingMonitor struct {
    37  	globals.Contextified
    38  	sync.Mutex
    39  	utils.DebugLabeler
    40  
    41  	timeout time.Duration
    42  	clock   clockwork.Clock
    43  	typers  map[string]*typingControlChans
    44  
    45  	// Testing
    46  	extendCh *chan struct{}
    47  }
    48  
    49  func NewTypingMonitor(g *globals.Context) *TypingMonitor {
    50  	return &TypingMonitor{
    51  		Contextified: globals.NewContextified(g),
    52  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "TypingMonitor", false),
    53  		typers:       make(map[string]*typingControlChans),
    54  		clock:        clockwork.NewRealClock(),
    55  		timeout:      typingTimeout,
    56  	}
    57  }
    58  
    59  func (t *TypingMonitor) SetClock(clock clockwork.Clock) {
    60  	t.clock = clock
    61  }
    62  
    63  func (t *TypingMonitor) SetTimeout(timeout time.Duration) {
    64  	t.timeout = timeout
    65  }
    66  
    67  func (t *TypingMonitor) key(typer chat1.TyperInfo, convID chat1.ConversationID) string {
    68  	return fmt.Sprintf("%s:%s:%s", typer.Uid, typer.DeviceID, convID)
    69  }
    70  
    71  func (t *TypingMonitor) convKey(key string, convID chat1.ConversationID) bool {
    72  	toks := strings.Split(key, ":")
    73  	if len(toks) != 3 {
    74  		return false
    75  	}
    76  	return toks[2] == convID.String()
    77  }
    78  
    79  func (t *TypingMonitor) notifyConvUpdateLocked(ctx context.Context, convID chat1.ConversationID) {
    80  	var typers []chat1.TyperInfo
    81  	for k, v := range t.typers {
    82  		if t.convKey(k, convID) {
    83  			typers = append(typers, v.typer)
    84  		}
    85  	}
    86  
    87  	update := chat1.ConvTypingUpdate{
    88  		ConvID: convID,
    89  		Typers: typers,
    90  	}
    91  	t.G().ActivityNotifier.TypingUpdate(ctx, []chat1.ConvTypingUpdate{update})
    92  }
    93  
    94  func (t *TypingMonitor) Update(ctx context.Context, typer chat1.TyperInfo, convID chat1.ConversationID,
    95  	teamType chat1.TeamType, typing bool) {
    96  
    97  	// If this is about ourselves, then don't bother
    98  	cuid := t.G().Env.GetUID()
    99  	cdid := t.G().Env.GetDeviceID()
   100  	if cuid.Equal(typer.Uid) && cdid.Eq(typer.DeviceID) {
   101  		return
   102  	}
   103  
   104  	// If the update is for a big team we are not currently viewing, don't bother sending it
   105  	if teamType == chat1.TeamType_COMPLEX && !t.G().Syncer.IsSelectedConversation(convID) {
   106  		return
   107  	}
   108  
   109  	// Process the update
   110  	t.Lock()
   111  	key := t.key(typer, convID)
   112  	chans, alreadyTyping := t.typers[key]
   113  	t.Unlock()
   114  	if typing {
   115  		if alreadyTyping {
   116  			// If this key is already typing, let's extend it
   117  			select {
   118  			case chans.extendCh <- struct{}{}:
   119  			default:
   120  				// This should never happen, but be safe
   121  				t.Debug(ctx, "Update: overflowed extend channel, dropping update: %s convID: %s", typer,
   122  					convID)
   123  			}
   124  		} else {
   125  			// Not typing yet, just add it in and spawn waiter
   126  			chans := newTypingControlChans(typer)
   127  			t.insertIntoTypers(ctx, key, chans, convID)
   128  			t.waitOnTyper(ctx, chans, convID)
   129  		}
   130  	} else if alreadyTyping {
   131  		// If they are typing, then stop it
   132  		select {
   133  		case chans.stopCh <- struct{}{}:
   134  		default:
   135  			// This should never happen, but be safe
   136  			t.Debug(ctx, "Update: overflowed stop channel, dropping update: %s convID: %s", typer,
   137  				convID)
   138  		}
   139  	}
   140  }
   141  
   142  func (t *TypingMonitor) insertIntoTypers(ctx context.Context, key string, chans *typingControlChans,
   143  	convID chat1.ConversationID) {
   144  	t.Lock()
   145  	defer t.Unlock()
   146  	t.typers[key] = chans
   147  	t.notifyConvUpdateLocked(ctx, convID)
   148  }
   149  
   150  func (t *TypingMonitor) removeFromTypers(ctx context.Context, key string, convID chat1.ConversationID) {
   151  	t.Lock()
   152  	defer t.Unlock()
   153  	delete(t.typers, key)
   154  	t.notifyConvUpdateLocked(ctx, convID)
   155  }
   156  
   157  func (t *TypingMonitor) waitOnTyper(ctx context.Context, chans *typingControlChans,
   158  	convID chat1.ConversationID) {
   159  	key := t.key(chans.typer, convID)
   160  	ctx = globals.BackgroundChatCtx(ctx, t.G())
   161  	deadline := t.clock.Now().Add(t.timeout)
   162  	go func() {
   163  		extends := 0
   164  		for {
   165  			select {
   166  			case <-t.clock.AfterTime(deadline):
   167  				// Send notifications and bail
   168  				t.removeFromTypers(ctx, key, convID)
   169  				return
   170  			case <-chans.extendCh:
   171  				// Loop around to restart timer
   172  				extends++
   173  				if extends > maxExtensions {
   174  					t.Debug(ctx, "waitOnTyper: max extensions reached: uid: %s convID: %s", chans.typer.Uid, convID)
   175  					t.removeFromTypers(ctx, key, convID)
   176  					return
   177  				}
   178  				deadline = t.clock.Now().Add(t.timeout)
   179  				if t.extendCh != nil {
   180  					// Alerts tests we extended time
   181  					*t.extendCh <- struct{}{}
   182  				}
   183  				continue
   184  			case <-chans.stopCh:
   185  				// Stopped typing, just end it and remove entry in typers
   186  				t.removeFromTypers(ctx, key, convID)
   187  				return
   188  			}
   189  		}
   190  	}()
   191  }