github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/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  }