github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/uithreadloader.go (about) 1 package chat 2 3 import ( 4 "context" 5 "encoding/base64" 6 "encoding/json" 7 "errors" 8 "sort" 9 "sync" 10 "time" 11 12 "github.com/keybase/client/go/chat/globals" 13 "github.com/keybase/client/go/chat/storage" 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/logger" 18 "github.com/keybase/client/go/protocol/chat1" 19 "github.com/keybase/client/go/protocol/gregor1" 20 "github.com/keybase/clockwork" 21 "github.com/keybase/go-codec/codec" 22 ) 23 24 type UIThreadLoader struct { 25 globals.Contextified 26 utils.DebugLabeler 27 sync.Mutex 28 29 clock clockwork.Clock 30 convPageStatus map[chat1.ConvIDStr]chat1.Pagination 31 validatedDelay time.Duration 32 offlineMu sync.Mutex 33 offline bool 34 connectedCh chan struct{} 35 ri func() chat1.RemoteInterface 36 37 activeConvLoadsMu sync.Mutex 38 activeConvLoads map[chat1.ConvIDStr]context.CancelFunc 39 40 // testing 41 cachedThreadDelay *time.Duration 42 remoteThreadDelay *time.Duration 43 resolveThreadDelay *time.Duration 44 } 45 46 func NewUIThreadLoader(g *globals.Context, ri func() chat1.RemoteInterface) *UIThreadLoader { 47 cacheDelay := 10 * time.Millisecond 48 return &UIThreadLoader{ 49 offline: false, 50 Contextified: globals.NewContextified(g), 51 DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "UIThreadLoader", false), 52 convPageStatus: make(map[chat1.ConvIDStr]chat1.Pagination), 53 clock: clockwork.NewRealClock(), 54 validatedDelay: 100 * time.Millisecond, 55 cachedThreadDelay: &cacheDelay, 56 activeConvLoads: make(map[chat1.ConvIDStr]context.CancelFunc), 57 connectedCh: make(chan struct{}), 58 ri: ri, 59 } 60 } 61 62 var _ types.UIThreadLoader = (*UIThreadLoader)(nil) 63 64 func (t *UIThreadLoader) Connected(ctx context.Context) { 65 t.offlineMu.Lock() 66 defer t.offlineMu.Unlock() 67 t.offline = false 68 select { 69 case t.connectedCh <- struct{}{}: 70 default: 71 } 72 } 73 74 func (t *UIThreadLoader) Disconnected(ctx context.Context) { 75 t.offlineMu.Lock() 76 defer t.offlineMu.Unlock() 77 t.offline = true 78 } 79 80 func (t *UIThreadLoader) SetRemoteInterface(ri func() chat1.RemoteInterface) { 81 t.ri = ri 82 } 83 84 func (t *UIThreadLoader) IsOffline(ctx context.Context) bool { 85 t.offlineMu.Lock() 86 defer t.offlineMu.Unlock() 87 return t.offline 88 } 89 90 func (t *UIThreadLoader) groupThreadView(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 91 tv chat1.ThreadView, dataSource types.InboxSourceDataSourceTyp) (res chat1.ThreadView, err error) { 92 // The following messages are consolidated for presentation 93 groupers := []msgGrouper{ 94 newJoinLeaveGrouper(t.G(), uid, convID, dataSource), 95 newBulkAddGrouper(t.G(), uid, convID, dataSource), 96 newChannelGrouper(t.G(), uid, convID, dataSource), 97 newAddedToTeamGrouper(t.G(), uid, convID, dataSource), 98 newErrGrouper(t.G(), uid, convID, dataSource), 99 } 100 for _, grouper := range groupers { 101 tv.Messages = groupGeneric(ctx, tv.Messages, grouper) 102 } 103 return tv, nil 104 } 105 106 func (t *UIThreadLoader) applyPagerModeIncoming(ctx context.Context, convID chat1.ConversationID, 107 pagination *chat1.Pagination, pgmode chat1.GetThreadNonblockPgMode) (res *chat1.Pagination) { 108 defer func() { 109 t.Debug(ctx, "applyPagerModeIncoming: mode: %v convID: %s xform: %s -> %s", pgmode, convID, 110 pagination, res) 111 }() 112 switch pgmode { 113 case chat1.GetThreadNonblockPgMode_SERVER: 114 if pagination == nil { 115 return nil 116 } 117 oldStored := t.convPageStatus[convID.ConvIDStr()] 118 if len(pagination.Next) > 0 { 119 return &chat1.Pagination{ 120 Num: pagination.Num, 121 Next: oldStored.Next, 122 Last: oldStored.Last, 123 } 124 } else if len(pagination.Previous) > 0 { 125 return &chat1.Pagination{ 126 Num: pagination.Num, 127 Previous: oldStored.Previous, 128 } 129 } 130 default: 131 // Nothing to do for other modes. 132 } 133 return pagination 134 } 135 136 func (t *UIThreadLoader) applyPagerModeOutgoing(ctx context.Context, convID chat1.ConversationID, 137 pagination *chat1.Pagination, incoming *chat1.Pagination, pgmode chat1.GetThreadNonblockPgMode) { 138 switch pgmode { 139 case chat1.GetThreadNonblockPgMode_SERVER: 140 if pagination == nil { 141 return 142 } 143 if incoming.FirstPage() { 144 t.Debug(ctx, "applyPagerModeOutgoing: resetting pagination: convID: %s p: %s", convID, pagination) 145 t.convPageStatus[convID.ConvIDStr()] = *pagination 146 } else { 147 oldStored := t.convPageStatus[convID.ConvIDStr()] 148 if len(incoming.Next) > 0 { 149 oldStored.Next = pagination.Next 150 t.Debug(ctx, "applyPagerModeOutgoing: setting next pagination: convID: %s p: %s", convID, 151 pagination) 152 } else if len(incoming.Previous) > 0 { 153 t.Debug(ctx, "applyPagerModeOutgoing: setting prev pagination: convID: %s p: %s", convID, 154 pagination) 155 oldStored.Previous = pagination.Previous 156 } 157 oldStored.Last = pagination.Last 158 t.convPageStatus[convID.ConvIDStr()] = oldStored 159 } 160 default: 161 // Nothing to do for other modes. 162 } 163 } 164 165 func (t *UIThreadLoader) messageIDControlToPagination(ctx context.Context, uid gregor1.UID, 166 convID chat1.ConversationID, msgIDControl chat1.MessageIDControl) *chat1.Pagination { 167 var mcconv *types.RemoteConversation 168 conv, err := utils.GetUnverifiedConv(ctx, t.G(), uid, convID, types.InboxSourceDataSourceLocalOnly) 169 if err != nil { 170 t.Debug(ctx, "messageIDControlToPagination: failed to get conversation: %s", err) 171 } else { 172 mcconv = &conv 173 } 174 return utils.MessageIDControlToPagination(ctx, t.DebugLabeler, &msgIDControl, mcconv) 175 } 176 177 func (t *UIThreadLoader) isConsolidateMsg(msg chat1.MessageUnboxed) bool { 178 if !msg.IsValid() { 179 return msg.IsError() 180 } 181 body := msg.Valid().MessageBody 182 typ, err := body.MessageType() 183 if err != nil { 184 return false 185 } 186 switch typ { 187 case chat1.MessageType_JOIN, chat1.MessageType_LEAVE, chat1.MessageType_SYSTEM: 188 return true 189 default: 190 return false 191 } 192 } 193 194 func (t *UIThreadLoader) mergeLocalRemoteThread(ctx context.Context, remoteThread, 195 localThread *chat1.ThreadView, mode chat1.GetThreadNonblockCbMode) (res chat1.ThreadView, err error) { 196 defer func() { 197 if err != nil || localThread == nil { 198 return 199 } 200 rm := make(map[chat1.MessageID]bool) 201 for _, m := range res.Messages { 202 rm[m.GetMessageID()] = true 203 } 204 // Check for any stray placeholders in the local thread we sent, and set them to some 205 // undisplayable type 206 for _, m := range localThread.Messages { 207 state, err := m.State() 208 if err != nil { 209 continue 210 } 211 if (state == chat1.MessageUnboxedState_PLACEHOLDER || t.isConsolidateMsg(m)) && 212 !rm[m.GetMessageID()] { 213 t.Debug(ctx, "mergeLocalRemoteThread: subbing in dead placeholder: msgID: %d", 214 m.GetMessageID()) 215 res.Messages = append(res.Messages, utils.CreateHiddenPlaceholder(m.GetMessageID())) 216 } 217 } 218 sort.Sort(utils.ByMsgUnboxedMsgID(res.Messages)) 219 }() 220 221 shouldAppend := func(newMsg chat1.MessageUnboxed, oldMsgs map[chat1.MessageID]chat1.MessageUnboxed) bool { 222 oldMsg, ok := oldMsgs[newMsg.GetMessageID()] 223 if !ok { 224 return true 225 } 226 // If either message is not valid, return the new one, something weird might be going on 227 if !oldMsg.IsValid() || !newMsg.IsValid() { 228 return true 229 } 230 // If this is a join message (or any other message that can get consolidated, then always 231 // transmit 232 if t.isConsolidateMsg(newMsg) { 233 return true 234 } 235 // If newMsg is now superseded by something different than what we sent, then let's include it 236 if newMsg.Valid().ServerHeader.SupersededBy != oldMsg.Valid().ServerHeader.SupersededBy { 237 t.Debug(ctx, "mergeLocalRemoteThread: including supersededBy change: msgID: %d", 238 newMsg.GetMessageID()) 239 return true 240 } 241 // If the message was exploded by someone, include it 242 if newMsg.Valid().ExplodedBy() != nil && 243 (oldMsg.Valid().ExplodedBy() == nil || *newMsg.Valid().ExplodedBy() != *oldMsg.Valid().ExplodedBy()) { 244 t.Debug(ctx, "mergeLocalRemoteThread: including explodedBy change: msgID: %d", 245 newMsg.GetMessageID()) 246 return true 247 } 248 // Any reactions or unfurl messages go 249 if newMsg.HasUnfurls() || oldMsg.HasUnfurls() || newMsg.HasReactions() || oldMsg.HasReactions() { 250 t.Debug(ctx, "mergeLocalRemoteThread: including reacted/unfurled msg: msgID: %d", 251 newMsg.GetMessageID()) 252 return true 253 } 254 // If replyTo is different, then let's also transmit this up 255 if newMsg.Valid().ReplyTo != oldMsg.Valid().ReplyTo { 256 return true 257 } 258 return false 259 } 260 switch mode { 261 case chat1.GetThreadNonblockCbMode_FULL: 262 return *remoteThread, nil 263 case chat1.GetThreadNonblockCbMode_INCREMENTAL: 264 if localThread != nil { 265 lm := make(map[chat1.MessageID]chat1.MessageUnboxed) 266 for _, m := range localThread.Messages { 267 lm[m.GetMessageID()] = m 268 } 269 res.Pagination = remoteThread.Pagination 270 for _, m := range remoteThread.Messages { 271 if shouldAppend(m, lm) { 272 res.Messages = append(res.Messages, m) 273 } 274 } 275 t.Debug(ctx, "mergeLocalRemoteThread: incremental cb mode: orig: %d post: %d", 276 len(remoteThread.Messages), len(res.Messages)) 277 return res, nil 278 } 279 return *remoteThread, nil 280 } 281 return res, errors.New("unknown get thread cb mode") 282 } 283 284 func (t *UIThreadLoader) dispatchOldPagesJob(ctx context.Context, uid gregor1.UID, 285 convID chat1.ConversationID, pagination *chat1.Pagination, resultPagination *chat1.Pagination) { 286 // Fire off pageback background jobs if we fetched the first page 287 num := 50 288 count := 3 289 if t.G().IsMobileAppType() { 290 num = 20 291 count = 1 292 } 293 if pagination.FirstPage() && resultPagination != nil && !resultPagination.Last { 294 p := &chat1.Pagination{ 295 Num: num, 296 Next: resultPagination.Next, 297 } 298 t.Debug(ctx, "dispatchOldPagesJob: queuing %s because of first page fetch: p: %s", convID, p) 299 if err := t.G().ConvLoader.Queue(ctx, types.NewConvLoaderJob(convID, p, 300 types.ConvLoaderPriorityLow, types.ConvLoaderGeneric, 301 newConvLoaderPagebackHook(t.G(), 0, count))); err != nil { 302 t.Debug(ctx, "dispatchOldPagesJob: failed to queue conversation load: %s", err) 303 } 304 } 305 } 306 307 func (t *UIThreadLoader) setUIStatus(ctx context.Context, chatUI libkb.ChatUI, 308 status chat1.UIChatThreadStatus, delay time.Duration) (cancelStatusFn func() bool) { 309 resCh := make(chan bool, 1) 310 ctx, cancelFn := context.WithCancel(ctx) 311 t.Debug(ctx, "setUIStatus: delaying: %v", delay) 312 go func(ctx context.Context) { 313 displayed := false 314 select { 315 case <-t.clock.After(delay): 316 select { 317 case <-ctx.Done(): 318 t.Debug(ctx, "setUIStatus: context canceled") 319 default: 320 if err := chatUI.ChatThreadStatus(context.Background(), status); err != nil { 321 t.Debug(ctx, "setUIStatus: failed to send: %s", err) 322 } 323 displayed = true 324 } 325 case <-ctx.Done(): 326 t.Debug(ctx, "setUIStatus: context canceled") 327 } 328 if displayed { 329 typ, _ := status.Typ() 330 t.Debug(ctx, "setUIStatus: displaying: %v", typ) 331 } 332 resCh <- displayed 333 }(ctx) 334 cancelStatusFn = func() bool { 335 cancelFn() 336 return <-resCh 337 } 338 return cancelStatusFn 339 } 340 341 func (t *UIThreadLoader) shouldIgnoreError(err error) bool { 342 switch terr := err.(type) { 343 case storage.AbortedError: 344 return true 345 case TransientUnboxingError: 346 return t.shouldIgnoreError(terr.Inner()) 347 } 348 switch err { 349 case context.Canceled: 350 return true 351 default: 352 } 353 return false 354 } 355 356 func (t *UIThreadLoader) noopCancel() {} 357 358 func (t *UIThreadLoader) singleFlightConv(ctx context.Context, convID chat1.ConversationID, 359 reason chat1.GetThreadReason) (context.Context, context.CancelFunc) { 360 t.activeConvLoadsMu.Lock() 361 defer t.activeConvLoadsMu.Unlock() 362 convIDStr := convID.ConvIDStr() 363 if cancel, ok := t.activeConvLoads[convIDStr]; ok { 364 cancel() 365 } 366 if reason == chat1.GetThreadReason_PUSH { 367 // we only cancel an outstanding load if it isn't from a push. The reason for this 368 // is that the push calls may come with remote message data from the push notification 369 // itself, and we don't want to replace that call with one that will not have that info. 370 return ctx, t.noopCancel 371 } 372 ctx, cancel := context.WithCancel(ctx) 373 t.activeConvLoads[convIDStr] = cancel 374 return ctx, cancel 375 } 376 377 func (t *UIThreadLoader) waitForOnline(ctx context.Context) (err error) { 378 defer func() { 379 // check for a canceled context before coming out of here 380 if err == nil { 381 select { 382 case <-ctx.Done(): 383 err = ctx.Err() 384 default: 385 } 386 } 387 }() 388 // wait at most a second, and then charge forward 389 for i := 0; i < 40; i++ { 390 if !t.IsOffline(ctx) { 391 return nil 392 } 393 select { 394 case <-ctx.Done(): 395 return ctx.Err() 396 case <-time.After(25 * time.Millisecond): 397 case <-t.connectedCh: 398 return nil 399 } 400 } 401 return nil 402 } 403 404 type knownRemoteInterface struct { 405 chat1.RemoteInterface 406 log logger.Logger 407 knownMap map[chat1.MessageID]chat1.MessageBoxed 408 conv types.RemoteConversation 409 } 410 411 func newKnownRemoteInterface(log logger.Logger, ri chat1.RemoteInterface, 412 conv types.RemoteConversation, knownMap map[chat1.MessageID]chat1.MessageBoxed) *knownRemoteInterface { 413 return &knownRemoteInterface{ 414 knownMap: knownMap, 415 log: log, 416 RemoteInterface: ri, 417 conv: conv, 418 } 419 } 420 421 func (i *knownRemoteInterface) GetMessagesRemote(ctx context.Context, arg chat1.GetMessagesRemoteArg) (res chat1.GetMessagesRemoteRes, err error) { 422 foundMap := make(map[chat1.MessageID]bool) 423 for _, msgID := range arg.MessageIDs { 424 if msgBoxed, ok := i.knownMap[msgID]; ok { 425 foundMap[msgID] = true 426 i.log.CDebugf(ctx, "knownRemoteInterface.GetMessagesRemote: hit message: %d", msgID) 427 res.Msgs = append(res.Msgs, msgBoxed) 428 } 429 } 430 var remoteFetch []chat1.MessageID 431 for _, msgID := range arg.MessageIDs { 432 if !foundMap[msgID] { 433 remoteFetch = append(remoteFetch, msgID) 434 } 435 } 436 res.MembersType = i.conv.GetMembersType() 437 res.Visibility = i.conv.Conv.Metadata.Visibility 438 if len(remoteFetch) == 0 { 439 return res, nil 440 } 441 remoteRes, err := i.RemoteInterface.GetMessagesRemote(ctx, chat1.GetMessagesRemoteArg{ 442 ConversationID: arg.ConversationID, 443 MessageIDs: remoteFetch, 444 ThreadReason: arg.ThreadReason, 445 }) 446 if err != nil { 447 return res, err 448 } 449 res.Msgs = append(res.Msgs, remoteRes.Msgs...) 450 res.RateLimit = remoteRes.RateLimit 451 return res, nil 452 } 453 454 func (t *UIThreadLoader) makeRi(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 455 knownRemotes []string) func() chat1.RemoteInterface { 456 if len(knownRemotes) == 0 { 457 t.Debug(ctx, "makeRi: no known remotes") 458 return t.ri 459 } 460 conv, err := utils.GetUnverifiedConv(ctx, t.G(), uid, convID, types.InboxSourceDataSourceLocalOnly) 461 if err != nil { 462 t.Debug(ctx, "makeRi: don't know about the conv") 463 return t.ri 464 } 465 t.Debug(ctx, "makeRi: creating new interface with %d known remotes", len(knownRemotes)) 466 knownMap := make(map[chat1.MessageID]chat1.MessageBoxed) 467 for _, knownRemote := range knownRemotes { 468 // Parse the message payload 469 bMsg, err := base64.StdEncoding.DecodeString(knownRemote) 470 if err != nil { 471 t.Debug(ctx, "makeRi: invalid message payload (skipping): %s", err) 472 continue 473 } 474 var msgBoxed chat1.MessageBoxed 475 mh := codec.MsgpackHandle{WriteExt: true} 476 if err = codec.NewDecoderBytes(bMsg, &mh).Decode(&msgBoxed); err != nil { 477 t.Debug(ctx, "makeRi: ifailed to msgpack decode payload (skipping): %s", err) 478 continue 479 } 480 knownMap[msgBoxed.GetMessageID()] = msgBoxed 481 } 482 return func() chat1.RemoteInterface { 483 return newKnownRemoteInterface(t.G().GetLog(), t.ri(), conv, knownMap) 484 } 485 } 486 487 func (t *UIThreadLoader) LoadNonblock(ctx context.Context, chatUI libkb.ChatUI, uid gregor1.UID, 488 convID chat1.ConversationID, reason chat1.GetThreadReason, pgmode chat1.GetThreadNonblockPgMode, 489 cbmode chat1.GetThreadNonblockCbMode, knownRemotes []string, query *chat1.GetThreadQuery, 490 uipagination *chat1.UIPagination) (err error) { 491 var pagination, resultPagination *chat1.Pagination 492 var fullErr error 493 defer t.Trace(ctx, &err, "LoadNonblock")() 494 defer func() { 495 // Detect any problem loading the thread, and queue it up in the retrier if there is a problem. 496 // Otherwise, send notice that we successfully loaded the conversation. 497 if fullErr != nil { 498 if t.shouldIgnoreError(fullErr) { 499 t.Debug(ctx, "LoadNonblock: ignoring error: %v", fullErr) 500 } else { 501 t.Debug(ctx, "LoadNonblock: queueing retry because of: %s", fullErr) 502 t.G().FetchRetrier.Failure(ctx, uid, 503 NewConversationRetry(t.G(), convID, nil, ThreadLoad)) 504 } 505 } else { 506 t.G().FetchRetrier.Success(ctx, uid, 507 NewConversationRetry(t.G(), convID, nil, ThreadLoad)) 508 // Load old pages of this conversation on success 509 t.dispatchOldPagesJob(ctx, uid, convID, pagination, resultPagination) 510 } 511 }() 512 // Set last select conversation on syncer 513 t.G().Syncer.SelectConversation(ctx, convID) 514 // Decode presentation form pagination 515 if pagination, err = utils.DecodePagination(uipagination); err != nil { 516 return err 517 } 518 519 // single flight per conv since the UI blasts this (only for first page) 520 outerCancel := func() {} 521 if pagination.FirstPage() { 522 ctx, outerCancel = t.singleFlightConv(ctx, convID, reason) 523 } 524 defer outerCancel() 525 526 // Lock conversation while this is running 527 if err := t.G().ConvSource.AcquireConversationLock(ctx, uid, convID); err != nil { 528 return err 529 } 530 defer t.G().ConvSource.ReleaseConversationLock(ctx, uid, convID) 531 t.Debug(ctx, "LoadNonblock: conversation lock obtained") 532 533 // Enable delete placeholders for supersede transform 534 if query == nil { 535 query = new(chat1.GetThreadQuery) 536 } 537 query.EnableDeletePlaceholders = true 538 539 // Parse out options 540 if query.MessageIDControl != nil { 541 // Pager control into pagination if given 542 t.Debug(ctx, "LoadNonblock: using message ID control for pagination: %v", *query.MessageIDControl) 543 pagination = t.messageIDControlToPagination(ctx, uid, convID, *query.MessageIDControl) 544 } else { 545 // Apply any pager mode transformations 546 pagination = t.applyPagerModeIncoming(ctx, convID, pagination, pgmode) 547 } 548 if pagination != nil && pagination.Last { 549 return nil 550 } 551 552 // Race the full operation versus the local one, so we don't lose anytime grabbing the local 553 // version if they are roughly as fast. However, the full operation has preference, so if it does 554 // win the race we don't send anything up from the local operation. 555 var localSentThread *chat1.ThreadView 556 var uilock sync.Mutex 557 var wg sync.WaitGroup 558 559 // Handle tracking status bar 560 displayedStatus := false 561 var uiStatusLock sync.Mutex 562 setDisplayedStatus := func(cancelUIStatus func() bool) { 563 status := cancelUIStatus() 564 uiStatusLock.Lock() 565 displayedStatus = displayedStatus || status 566 uiStatusLock.Unlock() 567 } 568 getDisplayedStatus := func() bool { 569 uiStatusLock.Lock() 570 defer uiStatusLock.Unlock() 571 return displayedStatus 572 } 573 574 localCtx, cancel := context.WithCancel(ctx) 575 wg.Add(1) 576 go func(ctx context.Context) { 577 defer wg.Done() 578 // Get local copy of the thread, abort the call if we have sent the full copy 579 var resThread *chat1.ThreadView 580 var localThread chat1.ThreadView 581 ch := make(chan error, 1) 582 go func() { 583 var err error 584 if t.cachedThreadDelay != nil { 585 select { 586 case <-t.clock.After(*t.cachedThreadDelay): 587 case <-ctx.Done(): 588 ch <- ctx.Err() 589 return 590 } 591 } 592 localThread, err = t.G().ConvSource.PullLocalOnly(ctx, convID, 593 uid, reason, query, pagination, 10) 594 ch <- err 595 }() 596 select { 597 case err := <-ch: 598 if err != nil { 599 t.Debug(ctx, "LoadNonblock: error running PullLocalOnly (sending miss): %s", err) 600 } else { 601 resThread = &localThread 602 } 603 case <-ctx.Done(): 604 t.Debug(ctx, "LoadNonblock: context canceled before PullLocalOnly returned") 605 return 606 } 607 uilock.Lock() 608 defer uilock.Unlock() 609 // Check this again, since we might have waited on the lock while full sent 610 select { 611 case <-ctx.Done(): 612 resThread = nil 613 t.Debug(ctx, "LoadNonblock: context canceled before local copy sent") 614 return 615 default: 616 } 617 var pthread *string 618 if resThread != nil { 619 *resThread, err = t.groupThreadView(ctx, uid, convID, *resThread, 620 types.InboxSourceDataSourceLocalOnly) 621 if err != nil { 622 t.Debug(ctx, "LoadNonblock: failed to group thread view: %v", err) 623 return 624 } 625 t.Debug(ctx, "LoadNonblock: sending cached response: messages: %d pager: %s", 626 len(resThread.Messages), resThread.Pagination) 627 localSentThread = resThread 628 pt := utils.PresentThreadView(ctx, t.G(), uid, *resThread, convID) 629 jsonPt, err := json.Marshal(pt) 630 if err != nil { 631 t.Debug(ctx, "LoadNonblock: failed to JSON cached response: %v", err) 632 return 633 } 634 sJSONPt := string(jsonPt) 635 pthread = &sJSONPt 636 t.applyPagerModeOutgoing(ctx, convID, resThread.Pagination, pagination, pgmode) 637 } else { 638 t.Debug(ctx, "LoadNonblock: sending nil cached response") 639 } 640 start := time.Now() 641 if err := chatUI.ChatThreadCached(ctx, pthread); err != nil { 642 t.Debug(ctx, "LoadNonblock: failed to send cached thread: %s", err) 643 } 644 t.Debug(ctx, "LoadNonblock: cached response send time: %v", time.Since(start)) 645 }(localCtx) 646 647 startTime := t.clock.Now() 648 baseDelay := 3 * time.Second 649 getDelay := func() time.Duration { 650 return baseDelay - (t.clock.Now().Sub(startTime)) 651 } 652 wg.Add(1) 653 go func() { 654 defer wg.Done() 655 // Run the full Pull operation, and redo pagination 656 ctx = globals.CtxModifyUnboxMode(ctx, types.UnboxModeQuick) 657 cancelUIStatus := t.setUIStatus(ctx, chatUI, chat1.NewUIChatThreadStatusWithServer(), getDelay()) 658 var remoteThread chat1.ThreadView 659 if t.remoteThreadDelay != nil { 660 t.clock.Sleep(*t.remoteThreadDelay) 661 } 662 // wait until we are online before attempting the full pull, otherwise we just waste an attempt 663 if fullErr = t.waitForOnline(ctx); fullErr != nil { 664 t.Debug(ctx, "LoadNonblock: waitForOnline error: %s", fullErr) 665 setDisplayedStatus(cancelUIStatus) 666 return 667 } 668 customRi := t.makeRi(ctx, uid, convID, knownRemotes) 669 remoteThread, fullErr = t.G().ConvSource.Pull(ctx, convID, uid, reason, customRi, query, pagination) 670 setDisplayedStatus(cancelUIStatus) 671 if fullErr != nil { 672 t.Debug(ctx, "LoadNonblock: error running Pull, returning error: %s", fullErr) 673 return 674 } 675 676 // Acquire lock and send up actual response 677 uilock.Lock() 678 defer uilock.Unlock() 679 var rthread chat1.ThreadView 680 remoteThread, fullErr = t.groupThreadView(ctx, uid, convID, remoteThread, 681 types.InboxSourceDataSourceAll) 682 if fullErr != nil { 683 return 684 } 685 if rthread, fullErr = 686 t.mergeLocalRemoteThread(ctx, &remoteThread, localSentThread, cbmode); fullErr != nil { 687 return 688 } 689 t.Debug(ctx, "LoadNonblock: presenting full response: messages: %d pager: %s", 690 len(rthread.Messages), rthread.Pagination) 691 start := time.Now() 692 uires := utils.PresentThreadView(ctx, t.G(), uid, rthread, convID) 693 t.Debug(ctx, "LoadNonblock: present compute time: %v", time.Since(start)) 694 var jsonUIRes []byte 695 if jsonUIRes, fullErr = json.Marshal(uires); fullErr != nil { 696 t.Debug(ctx, "LoadNonblock: failed to JSON full result: %s", fullErr) 697 return 698 } 699 resultPagination = rthread.Pagination 700 t.applyPagerModeOutgoing(ctx, convID, rthread.Pagination, pagination, pgmode) 701 start = time.Now() 702 if fullErr = chatUI.ChatThreadFull(ctx, string(jsonUIRes)); err != nil { 703 t.Debug(ctx, "LoadNonblock: failed to send full result to UI: %s", err) 704 return 705 } 706 t.Debug(ctx, "LoadNonblock: full response send time: %v", time.Since(start)) 707 708 // This means we transmitted with success, so cancel local thread 709 cancel() 710 }() 711 wg.Wait() 712 713 t.Debug(ctx, "LoadNonblock: thread payloads transferred, checking for resolve") 714 // Resolve any messages we didn't cache and get full information about 715 if fullErr == nil { 716 fullErr = func() error { 717 skips := globals.CtxMessageCacheSkips(ctx) 718 cancelUIStatus := t.setUIStatus(ctx, chatUI, chat1.NewUIChatThreadStatusWithValidating(0), 719 getDelay()) 720 defer func() { 721 setDisplayedStatus(cancelUIStatus) 722 }() 723 if t.resolveThreadDelay != nil { 724 t.clock.Sleep(*t.resolveThreadDelay) 725 } 726 rconv, err := utils.GetUnverifiedConv(ctx, t.G(), uid, convID, types.InboxSourceDataSourceAll) 727 if err != nil { 728 return err 729 } 730 for _, skip := range skips { 731 messages := skip.Msgs 732 if len(messages) == 0 { 733 continue 734 } 735 ctx = globals.CtxModifyUnboxMode(ctx, types.UnboxModeFull) 736 t.Debug(ctx, "LoadNonblock: resolving message skips: convID: %s num: %d", 737 skip.ConvID, len(messages)) 738 resolved, modifiedMap, err := NewBoxer(t.G()).ResolveSkippedUnboxeds(ctx, messages) 739 if err != nil { 740 return err 741 } 742 var pushConv types.RemoteConversation 743 if skip.ConvID.Eq(rconv.GetConvID()) { 744 pushConv = rconv 745 } else { 746 var err error 747 if pushConv, err = utils.GetUnverifiedConv(ctx, t.G(), uid, skip.ConvID, 748 types.InboxSourceDataSourceAll); err != nil { 749 return err 750 } 751 } 752 if err := t.G().ConvSource.PushUnboxed(ctx, pushConv, uid, resolved); err != nil { 753 return err 754 } 755 if !skip.ConvID.Eq(convID) { 756 // only deliver these updates for the current conv 757 continue 758 } 759 // filter resolved to only update changed messages 760 var changed []chat1.MessageUnboxed 761 for _, rmsg := range resolved { 762 if modifiedMap[rmsg.GetMessageID()] { 763 changed = append(changed, rmsg) 764 } 765 } 766 if len(changed) == 0 { 767 continue 768 } 769 var ierr error 770 maxDeletedUpTo := rconv.GetMaxDeletedUpTo() 771 if changed, ierr = t.G().ConvSource.TransformSupersedes(ctx, convID, uid, changed, 772 query, nil, nil, &maxDeletedUpTo); ierr != nil { 773 return ierr 774 } 775 notif := chat1.MessagesUpdated{ 776 ConvID: convID, 777 } 778 for _, msg := range changed { 779 if t.isConsolidateMsg(msg) { 780 // we don't want to update these, it just messes up consolidation 781 continue 782 } 783 notif.Updates = append(notif.Updates, utils.PresentMessageUnboxed(ctx, t.G(), msg, uid, 784 convID)) 785 } 786 act := chat1.NewChatActivityWithMessagesUpdated(notif) 787 t.G().ActivityNotifier.Activity(ctx, uid, chat1.TopicType_CHAT, 788 &act, chat1.ChatActivitySource_LOCAL) 789 } 790 return nil 791 }() 792 } 793 794 // Clean up context and set final loading status 795 if getDisplayedStatus() { 796 t.Debug(ctx, "LoadNonblock: status displayed, clearing") 797 t.clock.Sleep(t.validatedDelay) 798 // use a background context here in case our context has been canceled, we don't want to not 799 // get this banner off the screen. 800 if fullErr == nil { 801 t.Debug(ctx, "LoadNonblock: clearing with validated") 802 if err := chatUI.ChatThreadStatus(context.Background(), 803 chat1.NewUIChatThreadStatusWithValidated()); err != nil { 804 t.Debug(ctx, "LoadNonblock: failed to set status: %s", err) 805 } 806 } else { 807 t.Debug(ctx, "LoadNonblock: clearing with none") 808 if err := chatUI.ChatThreadStatus(context.Background(), 809 chat1.NewUIChatThreadStatusWithNone()); err != nil { 810 t.Debug(ctx, "LoadNonblock: failed to set status: %s", err) 811 } 812 } 813 t.Debug(ctx, "LoadNonblock: clear complete") 814 } else { 815 t.Debug(ctx, "LoadNonblock: no status displayed, not clearing") 816 } 817 818 cancel() 819 return fullErr 820 } 821 822 func (t *UIThreadLoader) Load(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 823 reason chat1.GetThreadReason, knownRemotes []string, query *chat1.GetThreadQuery, pagination *chat1.Pagination) (res chat1.ThreadView, err error) { 824 defer t.Trace(ctx, &err, "Load")() 825 // Xlate pager control into pagination if given 826 if query != nil && query.MessageIDControl != nil { 827 pagination = t.messageIDControlToPagination(ctx, uid, convID, 828 *query.MessageIDControl) 829 } 830 // Get messages from the source 831 ri := t.makeRi(ctx, uid, convID, knownRemotes) 832 return t.G().ConvSource.Pull(ctx, convID, uid, reason, ri, query, pagination) 833 }