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