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 }