github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/uiinboxloader.go (about) 1 package chat 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "sort" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/keybase/client/go/chat/globals" 14 "github.com/keybase/client/go/chat/types" 15 "github.com/keybase/client/go/chat/utils" 16 "github.com/keybase/client/go/libkb" 17 "github.com/keybase/client/go/protocol/chat1" 18 "github.com/keybase/client/go/protocol/gregor1" 19 "github.com/keybase/client/go/protocol/keybase1" 20 "github.com/keybase/clockwork" 21 "golang.org/x/sync/errgroup" 22 ) 23 24 type UIInboxLoader struct { 25 globals.Contextified 26 utils.DebugLabeler 27 sync.Mutex 28 29 uid gregor1.UID 30 stopCh chan struct{} 31 started bool 32 eg errgroup.Group 33 34 clock clockwork.Clock 35 transmitCh chan interface{} 36 layoutCh chan chat1.InboxLayoutReselectMode 37 bigTeamUnboxCh chan []chat1.ConversationID 38 convTransmitBatch map[chat1.ConvIDStr]chat1.ConversationLocal 39 batchDelay time.Duration 40 lastBatchFlush time.Time 41 lastLayoutFlush time.Time 42 smallTeamBound int 43 defaultSmallTeamBound int 44 45 // layout tracking 46 lastLayoutMu sync.Mutex 47 lastLayout *chat1.UIInboxLayout 48 49 // testing 50 testingLayoutForceMode bool 51 } 52 53 func NewUIInboxLoader(g *globals.Context) *UIInboxLoader { 54 defaultSmallTeamBound := 100 55 if g.IsMobileAppType() { 56 defaultSmallTeamBound = 50 57 } 58 return &UIInboxLoader{ 59 Contextified: globals.NewContextified(g), 60 DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "UIInboxLoader", false), 61 convTransmitBatch: make(map[chat1.ConvIDStr]chat1.ConversationLocal), 62 clock: clockwork.NewRealClock(), 63 batchDelay: 200 * time.Millisecond, 64 smallTeamBound: defaultSmallTeamBound, 65 defaultSmallTeamBound: defaultSmallTeamBound, 66 } 67 } 68 69 func (h *UIInboxLoader) Start(ctx context.Context, uid gregor1.UID) { 70 defer h.Trace(ctx, nil, "Start")() 71 h.Lock() 72 defer h.Unlock() 73 if h.started { 74 return 75 } 76 h.transmitCh = make(chan interface{}, 1000) 77 h.layoutCh = make(chan chat1.InboxLayoutReselectMode, 1000) 78 h.bigTeamUnboxCh = make(chan []chat1.ConversationID, 1000) 79 h.stopCh = make(chan struct{}) 80 h.started = true 81 h.uid = uid 82 h.eg.Go(func() error { return h.transmitLoop(h.stopCh) }) 83 h.eg.Go(func() error { return h.layoutLoop(h.stopCh) }) 84 h.eg.Go(func() error { return h.bigTeamUnboxLoop(h.stopCh) }) 85 } 86 87 func (h *UIInboxLoader) Stop(ctx context.Context) chan struct{} { 88 defer h.Trace(ctx, nil, "Stop")() 89 h.Lock() 90 defer h.Unlock() 91 ch := make(chan struct{}) 92 if h.started { 93 close(h.stopCh) 94 h.started = false 95 go func() { 96 err := h.eg.Wait() 97 if err != nil { 98 h.Debug(ctx, "Stop: error waiting: %+v", err) 99 } 100 close(ch) 101 }() 102 } else { 103 close(ch) 104 } 105 return ch 106 } 107 108 func (h *UIInboxLoader) getChatUI(ctx context.Context) (libkb.ChatUI, error) { 109 if h.G().UIRouter == nil { 110 return nil, errors.New("no UI router available") 111 } 112 ui, err := h.G().UIRouter.GetChatUI() 113 if err != nil { 114 return nil, err 115 } 116 if ui == nil { 117 h.Debug(ctx, "getChatUI: no chat UI found") 118 return nil, errors.New("no chat UI available") 119 } 120 return ui, nil 121 } 122 123 func (h *UIInboxLoader) presentUnverifiedInbox(ctx context.Context, convs []types.RemoteConversation, 124 offline bool) (res chat1.UnverifiedInboxUIItems, err error) { 125 for _, rawConv := range convs { 126 if len(rawConv.Conv.MaxMsgSummaries) == 0 { 127 h.Debug(ctx, "presentUnverifiedInbox: invalid convo, no max msg summaries, skipping: %s", 128 rawConv.Conv.GetConvID()) 129 continue 130 } 131 res.Items = append(res.Items, utils.PresentRemoteConversation(ctx, h.G(), h.uid, rawConv)) 132 } 133 res.Offline = offline 134 return res, err 135 } 136 137 type unverifiedResponse struct { 138 Convs []types.RemoteConversation 139 Query *chat1.GetInboxLocalQuery 140 Pagination *chat1.Pagination 141 } 142 143 type conversationResponse struct { 144 Conv chat1.ConversationLocal 145 } 146 147 type failedResponse struct { 148 Conv chat1.ConversationLocal 149 } 150 151 func (h *UIInboxLoader) flushConvBatch() (err error) { 152 if len(h.convTransmitBatch) == 0 { 153 return nil 154 } 155 ctx := globals.ChatCtx(context.Background(), h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, nil) 156 defer h.Trace(ctx, &err, "flushConvBatch")() 157 var convs []chat1.ConversationLocal 158 for _, conv := range h.convTransmitBatch { 159 convs = append(convs, conv) 160 } 161 h.lastBatchFlush = h.clock.Now() 162 h.convTransmitBatch = make(map[chat1.ConvIDStr]chat1.ConversationLocal) // clear batch always 163 h.Debug(ctx, "flushConvBatch: transmitting %d convs", len(convs)) 164 defer func() { 165 if err != nil { 166 h.Debug(ctx, "flushConvBatch: failed to transmit, retrying convs: num: %d err: %s", 167 len(convs), err) 168 for _, conv := range convs { 169 h.G().FetchRetrier.Failure(ctx, h.uid, 170 NewConversationRetry(h.G(), conv.GetConvID(), &conv.Info.Triple.Tlfid, InboxLoad)) 171 } 172 } 173 if err = h.G().InboxSource.MergeLocalMetadata(ctx, h.uid, convs); err != nil { 174 h.Debug(ctx, "flushConvBatch: unable to write inbox local metadata: %s", err) 175 } 176 }() 177 start := time.Now() 178 dat, err := json.Marshal(utils.PresentConversationLocals(ctx, h.G(), h.uid, convs, 179 utils.PresentParticipantsModeInclude)) 180 if err != nil { 181 return err 182 } 183 h.Debug(ctx, "flushConvBatch: present time: %v", time.Since(start)) 184 ui, err := h.getChatUI(ctx) 185 if err != nil { 186 return err 187 } 188 start = time.Now() 189 err = ui.ChatInboxConversation(ctx, chat1.ChatInboxConversationArg{ 190 Convs: string(dat), 191 }) 192 h.Debug(ctx, "flushConvBatch: transmit time: %v", time.Since(start)) 193 return err 194 } 195 196 func (h *UIInboxLoader) flushUnverified(r unverifiedResponse) (err error) { 197 ctx := context.Background() 198 defer func() { 199 if err != nil { 200 h.Debug(ctx, "flushUnverified: failed to transmit, retrying: %s", err) 201 h.G().FetchRetrier.Failure(ctx, h.uid, NewFullInboxRetry(h.G(), r.Query)) 202 } 203 }() 204 start := time.Now() 205 uires, err := h.presentUnverifiedInbox(ctx, r.Convs, h.G().InboxSource.IsOffline(ctx)) 206 if err != nil { 207 h.Debug(ctx, "flushUnverified: failed to present untrusted inbox, failing: %s", err.Error()) 208 return err 209 } 210 jbody, err := json.Marshal(uires) 211 if err != nil { 212 h.Debug(ctx, "flushUnverified: failed to JSON up unverified inbox: %s", err.Error()) 213 return err 214 } 215 h.Debug(ctx, "flushUnverified: present time: %v", time.Since(start)) 216 ui, err := h.getChatUI(ctx) 217 if err != nil { 218 return err 219 } 220 start = time.Now() 221 h.Debug(ctx, "flushUnverified: sending unverified inbox: num convs: %d bytes: %d", len(r.Convs), 222 len(jbody)) 223 if err := ui.ChatInboxUnverified(ctx, chat1.ChatInboxUnverifiedArg{ 224 Inbox: string(jbody), 225 }); err != nil { 226 h.Debug(ctx, "flushUnverified: failed to send unverfified inbox: %s", err) 227 return err 228 } 229 h.Debug(ctx, "flushUnverified: sent unverified inbox successfully: %v", time.Since(start)) 230 return nil 231 } 232 233 func (h *UIInboxLoader) flushFailed(r failedResponse) { 234 ctx := context.Background() 235 ui, err := h.getChatUI(ctx) 236 h.Debug(ctx, "flushFailed: transmitting: %s", r.Conv.GetConvID()) 237 if err == nil { 238 if err := ui.ChatInboxFailed(ctx, chat1.ChatInboxFailedArg{ 239 ConvID: r.Conv.GetConvID(), 240 Error: utils.PresentConversationErrorLocal(ctx, h.G(), h.uid, *r.Conv.Error), 241 }); err != nil { 242 h.Debug(ctx, "flushFailed: failed to send failed conv: %s", err) 243 } 244 } 245 // If we get a transient failure, add this to the retrier queue 246 if r.Conv.Error.Typ == chat1.ConversationErrorType_TRANSIENT { 247 h.G().FetchRetrier.Failure(ctx, h.uid, 248 NewConversationRetry(h.G(), r.Conv.GetConvID(), &r.Conv.Info.Triple.Tlfid, InboxLoad)) 249 } 250 } 251 252 func (h *UIInboxLoader) transmitOnce(imsg interface{}) { 253 switch msg := imsg.(type) { 254 case unverifiedResponse: 255 _ = h.flushConvBatch() 256 _ = h.flushUnverified(msg) 257 case failedResponse: 258 _ = h.flushConvBatch() 259 h.flushFailed(msg) 260 case conversationResponse: 261 h.convTransmitBatch[msg.Conv.GetConvID().ConvIDStr()] = msg.Conv 262 if h.clock.Since(h.lastBatchFlush) > h.batchDelay { 263 _ = h.flushConvBatch() 264 } 265 } 266 } 267 268 func (h *UIInboxLoader) transmitLoop(shutdownCh chan struct{}) error { 269 for { 270 select { 271 case msg := <-h.transmitCh: 272 h.transmitOnce(msg) 273 case <-h.clock.After(h.batchDelay): 274 _ = h.flushConvBatch() 275 case <-shutdownCh: 276 h.Debug(context.Background(), "transmitLoop: shutting down") 277 return nil 278 } 279 } 280 } 281 282 func (h *UIInboxLoader) LoadNonblock(ctx context.Context, query *chat1.GetInboxLocalQuery, 283 maxUnbox *int, skipUnverified bool) (err error) { 284 defer h.Trace(ctx, &err, "LoadNonblock")() 285 uid := h.uid 286 // Retry helpers 287 retryInboxLoad := func() { 288 h.G().FetchRetrier.Failure(ctx, uid, NewFullInboxRetry(h.G(), query)) 289 } 290 retryConvLoad := func(convID chat1.ConversationID, tlfID *chat1.TLFID) { 291 h.G().FetchRetrier.Failure(ctx, uid, NewConversationRetry(h.G(), convID, tlfID, InboxLoad)) 292 } 293 defer func() { 294 // handle errors on the main processing thread, any errors during localizaton are handled 295 // in the goroutine for localization callbacks 296 if err != nil { 297 if query != nil && len(query.ConvIDs) > 0 { 298 h.Debug(ctx, "LoadNonblock: failed to load convID query, retrying all convs") 299 for _, convID := range query.ConvIDs { 300 retryConvLoad(convID, nil) 301 } 302 } else { 303 h.Debug(ctx, "LoadNonblock: failed to load general query, retrying") 304 retryInboxLoad() 305 } 306 } 307 }() 308 309 // Invoke nonblocking inbox read and get remote inbox version to send back 310 // as our result 311 _, localizeCb, err := h.G().InboxSource.Read(ctx, uid, types.ConversationLocalizerNonblocking, 312 types.InboxSourceDataSourceAll, maxUnbox, query) 313 if err != nil { 314 return err 315 } 316 317 // Wait for inbox to get sent to us 318 var lres types.AsyncInboxResult 319 if skipUnverified { 320 select { 321 case lres = <-localizeCb: 322 h.Debug(ctx, "LoadNonblock: received unverified inbox, skipping send") 323 case <-time.After(time.Minute): 324 return fmt.Errorf("timeout waiting for inbox result") 325 case <-ctx.Done(): 326 h.Debug(ctx, "LoadNonblock: context canceled waiting for unverified (skip): %s") 327 return ctx.Err() 328 } 329 } else { 330 select { 331 case lres = <-localizeCb: 332 if lres.InboxRes == nil { 333 return fmt.Errorf("invalid conversation localize callback received") 334 } 335 h.transmitCh <- unverifiedResponse{ 336 Convs: lres.InboxRes.ConvsUnverified, 337 Query: query, 338 } 339 case <-time.After(time.Minute): 340 return fmt.Errorf("timeout waiting for inbox result") 341 case <-ctx.Done(): 342 h.Debug(ctx, "LoadNonblock: context canceled waiting for unverified") 343 return ctx.Err() 344 } 345 } 346 347 // Consume localize callbacks and send out to UI. 348 for convRes := range localizeCb { 349 go func(convRes types.AsyncInboxResult) { 350 if convRes.ConvLocal.Error != nil { 351 h.Debug(ctx, "LoadNonblock: *** error conv: id: %s err: %s", 352 convRes.Conv.ConvIDStr, convRes.ConvLocal.Error.Message) 353 h.transmitCh <- failedResponse{ 354 Conv: convRes.ConvLocal, 355 } 356 } else { 357 h.Debug(ctx, "LoadNonblock: success: conv: %s", convRes.Conv.ConvIDStr) 358 h.transmitCh <- conversationResponse{ 359 Conv: convRes.ConvLocal, 360 } 361 } 362 }(convRes) 363 } 364 return nil 365 } 366 367 func (h *UIInboxLoader) Query() chat1.GetInboxLocalQuery { 368 topicType := chat1.TopicType_CHAT 369 vis := keybase1.TLFVisibility_PRIVATE 370 return chat1.GetInboxLocalQuery{ 371 ComputeActiveList: true, 372 TopicType: &topicType, 373 TlfVisibility: &vis, 374 Status: []chat1.ConversationStatus{ 375 chat1.ConversationStatus_UNFILED, 376 chat1.ConversationStatus_FAVORITE, 377 chat1.ConversationStatus_MUTED, 378 }, 379 MemberStatus: []chat1.ConversationMemberStatus{ 380 chat1.ConversationMemberStatus_ACTIVE, 381 chat1.ConversationMemberStatus_PREVIEW, 382 chat1.ConversationMemberStatus_RESET, 383 }, 384 } 385 } 386 387 type bigTeam struct { 388 name string 389 id chat1.TLFIDStr 390 convs []types.RemoteConversation 391 } 392 393 func newBigTeam(name string, id chat1.TLFIDStr) *bigTeam { 394 return &bigTeam{name: name, id: id} 395 } 396 397 func (b *bigTeam) sort() { 398 sort.Slice(b.convs, func(i, j int) bool { 399 return strings.Compare(strings.ToLower(b.convs[i].GetTopicName()), 400 strings.ToLower(b.convs[j].GetTopicName())) < 0 401 }) 402 } 403 404 type bigTeamCollector struct { 405 teams map[string]*bigTeam 406 } 407 408 func newBigTeamCollector() *bigTeamCollector { 409 return &bigTeamCollector{ 410 teams: make(map[string]*bigTeam), 411 } 412 } 413 414 func (c *bigTeamCollector) appendConv(conv types.RemoteConversation) { 415 name := utils.GetRemoteConvTLFName(conv) 416 bt, ok := c.teams[name] 417 if !ok { 418 bt = newBigTeam(name, conv.Conv.Metadata.IdTriple.Tlfid.TLFIDStr()) 419 c.teams[name] = bt 420 } 421 bt.convs = append(bt.convs, conv) 422 } 423 424 func (c *bigTeamCollector) finalize(ctx context.Context) (res []chat1.UIInboxBigTeamRow) { 425 var bts []*bigTeam 426 for _, bt := range c.teams { 427 bt.sort() 428 bts = append(bts, bt) 429 } 430 sort.Slice(bts, func(i, j int) bool { 431 return strings.Compare(bts[i].name, bts[j].name) < 0 432 }) 433 for _, bt := range bts { 434 res = append(res, chat1.NewUIInboxBigTeamRowWithLabel(chat1.UIInboxBigTeamLabelRow{Name: bt.name, Id: bt.id})) 435 for _, conv := range bt.convs { 436 row := utils.PresentRemoteConversationAsBigTeamChannelRow(ctx, conv) 437 res = append(res, chat1.NewUIInboxBigTeamRowWithChannel(row)) 438 } 439 } 440 return res 441 } 442 443 func (h *UIInboxLoader) buildLayout(ctx context.Context, inbox types.Inbox, 444 reselectMode chat1.InboxLayoutReselectMode) (res chat1.UIInboxLayout) { 445 var widgetList []chat1.UIInboxSmallTeamRow 446 var btunboxes []chat1.ConversationID 447 btcollector := newBigTeamCollector() 448 selectedInLayout := false 449 selectedConv := h.G().Syncer.GetSelectedConversation() 450 username := h.G().Env.GetUsername().String() 451 for _, conv := range inbox.ConvsUnverified { 452 if conv.Conv.IsSelfFinalized(username) { 453 h.Debug(ctx, "buildLayout: skipping self finalized conv: %s", conv.ConvIDStr) 454 continue 455 } 456 if conv.GetConvID().Eq(selectedConv) { 457 selectedInLayout = true 458 } 459 switch conv.GetTeamType() { 460 case chat1.TeamType_COMPLEX: 461 if conv.LocalMetadata == nil { 462 btunboxes = append(btunboxes, conv.GetConvID()) 463 } 464 btcollector.appendConv(conv) 465 default: 466 // filter empty convs we didn't create 467 if utils.IsConvEmpty(conv.Conv) && conv.Conv.CreatorInfo != nil && 468 !conv.Conv.CreatorInfo.Uid.Eq(h.uid) { 469 continue 470 } 471 res.SmallTeams = append(res.SmallTeams, 472 utils.PresentRemoteConversationAsSmallTeamRow(ctx, conv, 473 h.G().GetEnv().GetUsername().String())) 474 } 475 widgetList = append(widgetList, utils.PresentRemoteConversationAsSmallTeamRow(ctx, conv, 476 h.G().GetEnv().GetUsername().String())) 477 } 478 sort.Slice(res.SmallTeams, func(i, j int) bool { 479 return res.SmallTeams[i].Time.After(res.SmallTeams[j].Time) 480 }) 481 res.BigTeams = btcollector.finalize(ctx) 482 res.TotalSmallTeams = len(res.SmallTeams) 483 if res.TotalSmallTeams > h.smallTeamBound { 484 res.SmallTeams = res.SmallTeams[:h.smallTeamBound] 485 // clear extra snippets to keep the payload size managable 486 for i := 50; i < len(res.SmallTeams); i++ { 487 res.SmallTeams[i].Snippet = nil 488 res.SmallTeams[i].SnippetDecoration = chat1.SnippetDecoration_NONE 489 } 490 } 491 if !selectedInLayout || reselectMode == chat1.InboxLayoutReselectMode_FORCE { 492 // select a new conv for the UI 493 var reselect chat1.UIInboxReselectInfo 494 reselect.OldConvID = selectedConv.ConvIDStr() 495 if len(res.SmallTeams) > 0 { 496 reselect.NewConvID = &res.SmallTeams[0].ConvID 497 } 498 h.Debug(ctx, "buildLayout: adding reselect info: %s", reselect) 499 res.ReselectInfo = &reselect 500 } 501 if !h.G().IsMobileAppType() { 502 badgeState := h.G().Badger.State() 503 sort.Slice(widgetList, func(i, j int) bool { 504 ibadged := badgeState.ConversationBadgeStr(ctx, widgetList[i].ConvID) > 0 505 jbadged := badgeState.ConversationBadgeStr(ctx, widgetList[j].ConvID) > 0 506 if ibadged && !jbadged { 507 return true 508 } else if !ibadged && jbadged { 509 return false 510 } else { 511 return widgetList[i].Time.After(widgetList[j].Time) 512 } 513 }) 514 // only set widget entries on desktop to the top 3 overall convs 515 if len(widgetList) > 5 { 516 res.WidgetList = widgetList[:5] 517 } else { 518 res.WidgetList = widgetList 519 } 520 } 521 if len(btunboxes) > 0 { 522 h.Debug(ctx, "buildLayout: big teams missing names, unboxing: %v", len(btunboxes)) 523 h.queueBigTeamUnbox(btunboxes) 524 } 525 return res 526 } 527 528 func (h *UIInboxLoader) getInboxFromQuery(ctx context.Context) (inbox types.Inbox, err error) { 529 defer h.Trace(ctx, &err, "getInboxFromQuery")() 530 query := h.Query() 531 rquery, _, err := h.G().InboxSource.GetInboxQueryLocalToRemote(ctx, &query) 532 if err != nil { 533 return inbox, err 534 } 535 return h.G().InboxSource.ReadUnverified(ctx, h.uid, types.InboxSourceDataSourceAll, rquery) 536 } 537 538 func (h *UIInboxLoader) flushLayout(reselectMode chat1.InboxLayoutReselectMode) (err error) { 539 ctx := globals.ChatCtx(context.Background(), h.G(), keybase1.TLFIdentifyBehavior_GUI, nil, nil) 540 defer h.Trace(ctx, &err, "flushLayout")() 541 defer func() { 542 if err != nil { 543 h.Debug(ctx, "flushLayout: failed to transmit, retrying: %s", err) 544 q := h.Query() 545 h.G().FetchRetrier.Failure(ctx, h.uid, NewFullInboxRetry(h.G(), &q)) 546 } 547 }() 548 ui, err := h.getChatUI(ctx) 549 if err != nil { 550 h.Debug(ctx, "flushLayout: no chat UI available, skipping send") 551 return nil 552 } 553 inbox, err := h.getInboxFromQuery(ctx) 554 if err != nil { 555 return err 556 } 557 layout := h.buildLayout(ctx, inbox, reselectMode) 558 dat, err := json.Marshal(layout) 559 if err != nil { 560 return err 561 } 562 if err := ui.ChatInboxLayout(ctx, string(dat)); err != nil { 563 return err 564 } 565 h.setLastLayout(&layout) 566 return nil 567 } 568 569 func (h *UIInboxLoader) queueBigTeamUnbox(convIDs []chat1.ConversationID) { 570 select { 571 case h.bigTeamUnboxCh <- convIDs: 572 default: 573 h.Debug(context.Background(), "queueBigTeamUnbox: failed to queue big team unbox, queue full") 574 } 575 } 576 577 func (h *UIInboxLoader) bigTeamUnboxLoop(shutdownCh chan struct{}) error { 578 ctx := globals.ChatCtx(context.Background(), h.G(), keybase1.TLFIdentifyBehavior_GUI, nil, nil) 579 for { 580 select { 581 case convIDs := <-h.bigTeamUnboxCh: 582 doneCh := make(chan struct{}) 583 ctx, cancel := context.WithCancel(ctx) 584 go func(ctx context.Context) { 585 defer close(doneCh) 586 h.Debug(ctx, "bigTeamUnboxLoop: pulled %d convs to unbox", len(convIDs)) 587 if err := h.UpdateConvs(ctx, convIDs); err != nil { 588 h.Debug(ctx, "bigTeamUnboxLoop: unbox convs error: %s", err) 589 } 590 // update layout again after we have done all this work to get everything in the right order 591 h.UpdateLayout(ctx, chat1.InboxLayoutReselectMode_DEFAULT, "big team unbox") 592 }(ctx) 593 select { 594 case <-doneCh: 595 case <-shutdownCh: 596 h.Debug(ctx, "bigTeamUnboxLoop: shutdown during unboxing, going down") 597 } 598 cancel() 599 case <-shutdownCh: 600 h.Debug(ctx, "bigTeamUnboxLoop: shutting down") 601 return nil 602 } 603 } 604 } 605 606 func (h *UIInboxLoader) layoutLoop(shutdownCh chan struct{}) error { 607 var shouldFlush bool 608 var lastReselectMode chat1.InboxLayoutReselectMode 609 reset := func() { 610 shouldFlush = false 611 lastReselectMode = chat1.InboxLayoutReselectMode_DEFAULT 612 } 613 reset() 614 for { 615 select { 616 case reselectMode := <-h.layoutCh: 617 if reselectMode == chat1.InboxLayoutReselectMode_FORCE { 618 lastReselectMode = reselectMode 619 } 620 if h.clock.Since(h.lastLayoutFlush) > h.batchDelay || h.testingLayoutForceMode { 621 _ = h.flushLayout(lastReselectMode) 622 reset() 623 } else { 624 shouldFlush = true 625 } 626 case <-h.clock.After(h.batchDelay): 627 if shouldFlush { 628 _ = h.flushLayout(lastReselectMode) 629 reset() 630 } 631 case <-shutdownCh: 632 h.Debug(context.Background(), "layoutLoop: shutting down") 633 return nil 634 } 635 } 636 } 637 638 func (h *UIInboxLoader) isTopSmallTeamInLastLayout(convID chat1.ConversationID) bool { 639 h.lastLayoutMu.Lock() 640 defer h.lastLayoutMu.Unlock() 641 if h.lastLayout == nil { 642 return false 643 } 644 if len(h.lastLayout.SmallTeams) == 0 { 645 return false 646 } 647 return h.lastLayout.SmallTeams[0].ConvID == convID.ConvIDStr() 648 } 649 650 func (h *UIInboxLoader) setLastLayout(l *chat1.UIInboxLayout) { 651 h.lastLayoutMu.Lock() 652 defer h.lastLayoutMu.Unlock() 653 h.lastLayout = l 654 } 655 656 func (h *UIInboxLoader) UpdateLayout(ctx context.Context, reselectMode chat1.InboxLayoutReselectMode, 657 reason string) { 658 defer h.Trace(ctx, nil, "UpdateLayout: %s", reason)() 659 select { 660 case h.layoutCh <- reselectMode: 661 default: 662 h.Debug(ctx, "failed to queue layout update, queue full") 663 } 664 } 665 666 func (h *UIInboxLoader) UpdateLayoutFromNewMessage(ctx context.Context, conv types.RemoteConversation) { 667 defer h.Trace(ctx, nil, "UpdateLayoutFromNewMessage: %s", conv.ConvIDStr)() 668 if h.isTopSmallTeamInLastLayout(conv.GetConvID()) { 669 h.Debug(ctx, "UpdateLayoutFromNewMessage: skipping layout, conv top small team in last layout") 670 } else if conv.GetTeamType() == chat1.TeamType_COMPLEX { 671 h.Debug(ctx, "UpdateLayoutFromNewMessage: skipping layout, complex team conv") 672 } else { 673 h.UpdateLayout(ctx, chat1.InboxLayoutReselectMode_DEFAULT, "new message") 674 } 675 } 676 677 func (h *UIInboxLoader) UpdateLayoutFromSubteamRename(ctx context.Context, convs []types.RemoteConversation) { 678 defer h.Trace(ctx, nil, "UpdateLayoutFromSubteamRename")() 679 var bigTeamConvs []chat1.ConversationID 680 for _, conv := range convs { 681 if conv.GetTeamType() == chat1.TeamType_COMPLEX { 682 bigTeamConvs = append(bigTeamConvs, conv.GetConvID()) 683 } 684 } 685 if len(bigTeamConvs) > 0 { 686 h.queueBigTeamUnbox(bigTeamConvs) 687 } 688 } 689 690 func (h *UIInboxLoader) UpdateConvs(ctx context.Context, convIDs []chat1.ConversationID) (err error) { 691 defer h.Trace(ctx, &err, "UpdateConvs")() 692 query := chat1.GetInboxLocalQuery{ 693 ComputeActiveList: true, 694 ConvIDs: convIDs, 695 } 696 return h.LoadNonblock(ctx, &query, nil, true) 697 } 698 699 func (h *UIInboxLoader) UpdateLayoutFromSmallIncrease(ctx context.Context) { 700 defer h.Trace(ctx, nil, "UpdateLayoutFromSmallIncrease")() 701 h.smallTeamBound += h.defaultSmallTeamBound 702 h.UpdateLayout(ctx, chat1.InboxLayoutReselectMode_DEFAULT, "small increase") 703 } 704 705 func (h *UIInboxLoader) UpdateLayoutFromSmallReset(ctx context.Context) { 706 defer h.Trace(ctx, nil, "UpdateLayoutFromSmallReset")() 707 h.smallTeamBound = h.defaultSmallTeamBound 708 h.UpdateLayout(ctx, chat1.InboxLayoutReselectMode_DEFAULT, "small reset") 709 }