github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/inboxsource.go (about)

     1  package chat
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"sort"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/keybase/client/go/chat/globals"
    12  	"github.com/keybase/client/go/chat/storage"
    13  	"github.com/keybase/client/go/chat/types"
    14  	"github.com/keybase/client/go/chat/utils"
    15  	"github.com/keybase/client/go/libkb"
    16  	"github.com/keybase/client/go/protocol/chat1"
    17  	"github.com/keybase/client/go/protocol/gregor1"
    18  	"github.com/keybase/client/go/protocol/keybase1"
    19  	"github.com/keybase/client/go/teams/opensearch"
    20  	context "golang.org/x/net/context"
    21  	"golang.org/x/sync/errgroup"
    22  )
    23  
    24  func filterConvLocals(convLocals []chat1.ConversationLocal, rquery *chat1.GetInboxQuery,
    25  	query *chat1.GetInboxLocalQuery, nameInfo types.NameInfo) (res []chat1.ConversationLocal, err error) {
    26  	res = make([]chat1.ConversationLocal, 0, len(convLocals))
    27  	for _, convLocal := range convLocals {
    28  		if rquery != nil && rquery.TlfID != nil {
    29  			// inbox query contained a TLF name, so check to make sure that
    30  			// the conversation from the server matches tlfInfo from kbfs
    31  			if len(nameInfo.CanonicalName) > 0 &&
    32  				convLocal.Info.TLFNameExpanded() != nameInfo.CanonicalName {
    33  				if convLocal.Error == nil {
    34  					return nil, fmt.Errorf("server conversation TLF name mismatch: %s, expected %s",
    35  						convLocal.Info.TLFNameExpanded(), nameInfo.CanonicalName)
    36  				}
    37  			}
    38  			if convLocal.Info.Visibility != rquery.Visibility() {
    39  				return nil, fmt.Errorf("server conversation TLF visibility mismatch: %s, expected %s",
    40  					convLocal.Info.Visibility, rquery.Visibility())
    41  			}
    42  			if !nameInfo.ID.Eq(convLocal.Info.Triple.Tlfid) {
    43  				return nil, fmt.Errorf("server conversation TLF ID mismatch: %s, expected %s",
    44  					convLocal.Info.Triple.Tlfid, nameInfo.ID)
    45  			}
    46  			// tlfInfo.ID and rquery.TlfID should always match, but just in case:
    47  			if !rquery.TlfID.Eq(convLocal.Info.Triple.Tlfid) {
    48  				return nil, fmt.Errorf("server conversation TLF ID mismatch: %s, expected %s",
    49  					convLocal.Info.Triple.Tlfid, rquery.TlfID)
    50  			}
    51  
    52  			// Note that previously, we made a call to KBFS to lookup the TLF in
    53  			// convLocal.Info.TlfName and verify that, but the above checks accomplish
    54  			// the same thing without an RPC call.
    55  		}
    56  
    57  		// server can't query on topic name, so we have to do it ourselves in the loop
    58  		if query != nil && query.TopicName != nil && *query.TopicName != convLocal.Info.TopicName {
    59  			continue
    60  		}
    61  
    62  		res = append(res, convLocal)
    63  	}
    64  
    65  	return res, nil
    66  }
    67  
    68  type baseInboxSource struct {
    69  	globals.Contextified
    70  	utils.DebugLabeler
    71  
    72  	so               *sourceOfflinable
    73  	sub              types.InboxSource
    74  	getChatInterface func() chat1.RemoteInterface
    75  	localizer        *localizerPipeline
    76  }
    77  
    78  func newBaseInboxSource(g *globals.Context, ibs types.InboxSource,
    79  	getChatInterface func() chat1.RemoteInterface) *baseInboxSource {
    80  	labeler := utils.NewDebugLabeler(g.ExternalG(), "baseInboxSource", false)
    81  	return &baseInboxSource{
    82  		Contextified:     globals.NewContextified(g),
    83  		sub:              ibs,
    84  		DebugLabeler:     labeler,
    85  		getChatInterface: getChatInterface,
    86  		so:               newSourceOfflinable(g, labeler),
    87  		localizer:        newLocalizerPipeline(g),
    88  	}
    89  }
    90  
    91  func (b *baseInboxSource) notifyTlfFinalize(ctx context.Context, username string) {
    92  	// Let the rest of the system know this user has changed
    93  	arg := libkb.NewLoadUserArg(b.G().ExternalG()).WithName(username).WithPublicKeyOptional()
    94  	finalizeUser, err := libkb.LoadUser(arg)
    95  	if err != nil {
    96  		b.Debug(ctx, "notifyTlfFinalize: failed to load finalize user, skipping user changed notification: err: %s", err.Error())
    97  	} else {
    98  		b.G().UserChanged(ctx, finalizeUser.GetUID())
    99  	}
   100  }
   101  
   102  func (b *baseInboxSource) SetRemoteInterface(ri func() chat1.RemoteInterface) {
   103  	b.getChatInterface = ri
   104  }
   105  
   106  func (b *baseInboxSource) GetInboxQueryLocalToRemote(ctx context.Context,
   107  	lquery *chat1.GetInboxLocalQuery) (rquery *chat1.GetInboxQuery, info types.NameInfo, err error) {
   108  
   109  	if lquery == nil {
   110  		return nil, info, nil
   111  	}
   112  
   113  	rquery = &chat1.GetInboxQuery{}
   114  	if lquery.Name != nil && lquery.Name.TlfID != nil && len(lquery.Name.Name) > 0 {
   115  		rquery.TlfID = lquery.Name.TlfID
   116  		rquery.MembersTypes = []chat1.ConversationMembersType{lquery.Name.MembersType}
   117  		info = types.NameInfo{
   118  			CanonicalName: lquery.Name.Name,
   119  			ID:            *lquery.Name.TlfID,
   120  		}
   121  		b.Debug(ctx, "GetInboxQueryLocalToRemote: using TLFID: %v", *lquery.Name.TlfID)
   122  	} else if lquery.Name != nil && lquery.Name.TlfID != nil {
   123  		rquery.TlfID = lquery.Name.TlfID
   124  		info = types.NameInfo{
   125  			ID: *lquery.Name.TlfID,
   126  		}
   127  		b.Debug(ctx, "GetInboxQueryLocalToRemote: using TLFID (nameless): %v", *lquery.Name.TlfID)
   128  	} else if lquery.Name != nil && len(lquery.Name.Name) > 0 {
   129  		var err error
   130  		tlfName := utils.AddUserToTLFName(b.G(), lquery.Name.Name, lquery.Visibility(),
   131  			lquery.Name.MembersType)
   132  		info, err = CreateNameInfoSource(ctx, b.G(), lquery.Name.MembersType).LookupID(ctx, tlfName,
   133  			lquery.Visibility() == keybase1.TLFVisibility_PUBLIC)
   134  		if err != nil {
   135  			b.Debug(ctx, "GetInboxQueryLocalToRemote: failed: %s", err)
   136  			return nil, info, err
   137  		}
   138  		rquery.TlfID = &info.ID
   139  		rquery.MembersTypes = []chat1.ConversationMembersType{lquery.Name.MembersType}
   140  		b.Debug(ctx, "GetInboxQueryLocalToRemote: mapped name %q to TLFID %v", tlfName, info.ID)
   141  	}
   142  	rquery.TopicName = lquery.TopicName
   143  	rquery.After = lquery.After
   144  	rquery.Before = lquery.Before
   145  	rquery.TlfVisibility = lquery.TlfVisibility
   146  	rquery.TopicType = lquery.TopicType
   147  	rquery.UnreadOnly = lquery.UnreadOnly
   148  	rquery.ReadOnly = lquery.ReadOnly
   149  	rquery.ComputeActiveList = lquery.ComputeActiveList
   150  	rquery.ConvIDs = lquery.ConvIDs
   151  	rquery.OneChatTypePerTLF = lquery.OneChatTypePerTLF
   152  	rquery.Status = lquery.Status
   153  	rquery.MemberStatus = lquery.MemberStatus
   154  	rquery.SummarizeMaxMsgs = false
   155  
   156  	return rquery, info, nil
   157  }
   158  
   159  func (b *baseInboxSource) IsMember(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID) (bool, error) {
   160  	conv, err := utils.GetUnverifiedConv(ctx, b.G(), uid, convID, types.InboxSourceDataSourceAll)
   161  	if err != nil {
   162  		return false, err
   163  	}
   164  	switch conv.Conv.ReaderInfo.Status {
   165  	case chat1.ConversationMemberStatus_ACTIVE, chat1.ConversationMemberStatus_RESET:
   166  		return true, nil
   167  	default:
   168  		return false, nil
   169  	}
   170  }
   171  
   172  func (b *baseInboxSource) Localize(ctx context.Context, uid gregor1.UID, convs []types.RemoteConversation,
   173  	localizerTyp types.ConversationLocalizerTyp) ([]chat1.ConversationLocal, chan types.AsyncInboxResult, error) {
   174  	localizeCb := make(chan types.AsyncInboxResult, len(convs))
   175  	localizer := b.createConversationLocalizer(ctx, localizerTyp, localizeCb)
   176  	b.Debug(ctx, "Localize: using localizer: %s, convs: %d", localizer.Name(), len(convs))
   177  
   178  	res, err := localizer.Localize(ctx, uid, types.Inbox{
   179  		ConvsUnverified: convs,
   180  	}, nil)
   181  	return res, localizeCb, err
   182  }
   183  
   184  func (b *baseInboxSource) RemoteSetConversationStatus(ctx context.Context, uid gregor1.UID,
   185  	convID chat1.ConversationID, status chat1.ConversationStatus) (err error) {
   186  	defer b.Trace(ctx, &err, "RemoteSetConversationStatus")()
   187  	if _, err = b.getChatInterface().SetConversationStatus(ctx, chat1.SetConversationStatusArg{
   188  		ConversationID: convID,
   189  		Status:         status,
   190  	}); err != nil {
   191  		return err
   192  	}
   193  	return nil
   194  }
   195  
   196  func (b *baseInboxSource) RemoteDeleteConversation(ctx context.Context, uid gregor1.UID,
   197  	convID chat1.ConversationID) (err error) {
   198  	defer b.Trace(ctx, &err, "RemoteDeleteConversation")()
   199  	if _, err = b.getChatInterface().DeleteConversation(ctx, convID); err != nil {
   200  		return err
   201  	}
   202  	return nil
   203  }
   204  
   205  func (b *baseInboxSource) createConversationLocalizer(ctx context.Context, typ types.ConversationLocalizerTyp,
   206  	localizeCb chan types.AsyncInboxResult) conversationLocalizer {
   207  	switch typ {
   208  	case types.ConversationLocalizerBlocking:
   209  		return newBlockingLocalizer(b.G(), b.localizer, localizeCb)
   210  	case types.ConversationLocalizerNonblocking:
   211  		return newNonblockingLocalizer(b.G(), b.localizer, localizeCb)
   212  	default:
   213  		b.Debug(ctx, "createConversationLocalizer: warning unknown typ %v, using blockingLocalizer as default", typ)
   214  		return newBlockingLocalizer(b.G(), b.localizer, localizeCb)
   215  	}
   216  }
   217  
   218  func (b *baseInboxSource) setDefaultParticipantMode(q *chat1.GetInboxQuery) *chat1.GetInboxQuery {
   219  	if q == nil {
   220  		q = new(chat1.GetInboxQuery)
   221  	}
   222  	q.ParticipantsMode = chat1.InboxParticipantsMode_SKIP_TEAMS
   223  	return q
   224  }
   225  
   226  func (b *baseInboxSource) Start(ctx context.Context, uid gregor1.UID) {
   227  	b.localizer.start(ctx)
   228  }
   229  
   230  func (b *baseInboxSource) Stop(ctx context.Context) chan struct{} {
   231  	return b.localizer.stop(ctx)
   232  }
   233  
   234  func (b *baseInboxSource) Suspend(ctx context.Context) bool {
   235  	return b.localizer.suspend(ctx)
   236  }
   237  
   238  func (b *baseInboxSource) Resume(ctx context.Context) bool {
   239  	return b.localizer.resume(ctx)
   240  }
   241  
   242  func (b *baseInboxSource) IsOffline(ctx context.Context) bool {
   243  	return b.so.IsOffline(ctx)
   244  }
   245  
   246  func (b *baseInboxSource) Connected(ctx context.Context) {
   247  	b.so.Connected(ctx)
   248  	b.localizer.Connected()
   249  }
   250  
   251  func (b *baseInboxSource) Disconnected(ctx context.Context) {
   252  	b.so.Disconnected(ctx)
   253  	b.localizer.Disconnected()
   254  }
   255  
   256  func (b *baseInboxSource) ApplyLocalChatState(ctx context.Context, i []keybase1.BadgeConversationInfo) ([]keybase1.BadgeConversationInfo, int, int) {
   257  	return i, 0, 0
   258  }
   259  
   260  func GetInboxQueryNameInfo(ctx context.Context, g *globals.Context,
   261  	lquery *chat1.GetInboxLocalQuery) (res types.NameInfo, err error) {
   262  	if lquery.Name == nil {
   263  		return res, errors.New("invalid name query")
   264  	} else if lquery.Name != nil && len(lquery.Name.Name) > 0 {
   265  		if lquery.Name.TlfID != nil {
   266  			return CreateNameInfoSource(ctx, g, lquery.Name.MembersType).LookupName(ctx, *lquery.Name.TlfID,
   267  				lquery.Visibility() == keybase1.TLFVisibility_PUBLIC, lquery.Name.Name)
   268  		}
   269  		return CreateNameInfoSource(ctx, g, lquery.Name.MembersType).LookupID(ctx, lquery.Name.Name,
   270  			lquery.Visibility() == keybase1.TLFVisibility_PUBLIC)
   271  	} else {
   272  		return res, errors.New("invalid name query")
   273  	}
   274  }
   275  
   276  type RemoteInboxSource struct {
   277  	globals.Contextified
   278  	utils.DebugLabeler
   279  	*baseInboxSource
   280  }
   281  
   282  var _ types.InboxSource = (*RemoteInboxSource)(nil)
   283  
   284  func NewRemoteInboxSource(g *globals.Context, ri func() chat1.RemoteInterface) *RemoteInboxSource {
   285  	labeler := utils.NewDebugLabeler(g.ExternalG(), "RemoteInboxSource", false)
   286  	s := &RemoteInboxSource{
   287  		Contextified: globals.NewContextified(g),
   288  		DebugLabeler: labeler,
   289  	}
   290  	s.baseInboxSource = newBaseInboxSource(g, s, ri)
   291  	return s
   292  }
   293  
   294  func (s *RemoteInboxSource) Clear(ctx context.Context, uid gregor1.UID, opts *types.ClearOpts) error {
   295  	return nil
   296  }
   297  
   298  func (s *RemoteInboxSource) Read(ctx context.Context, uid gregor1.UID,
   299  	localizerTyp types.ConversationLocalizerTyp, dataSource types.InboxSourceDataSourceTyp, maxLocalize *int,
   300  	query *chat1.GetInboxLocalQuery) (types.Inbox, chan types.AsyncInboxResult, error) {
   301  
   302  	rquery, tlfInfo, err := s.GetInboxQueryLocalToRemote(ctx, query)
   303  	if err != nil {
   304  		return types.Inbox{}, nil, err
   305  	}
   306  	inbox, err := s.ReadUnverified(ctx, uid, dataSource, rquery)
   307  	if err != nil {
   308  		return types.Inbox{}, nil, err
   309  	}
   310  
   311  	localizeCb := make(chan types.AsyncInboxResult, len(inbox.ConvsUnverified))
   312  	localizer := s.createConversationLocalizer(ctx, localizerTyp, localizeCb)
   313  	s.Debug(ctx, "Read: using localizer: %s", localizer.Name())
   314  
   315  	res, err := localizer.Localize(ctx, uid, inbox, maxLocalize)
   316  	if err != nil {
   317  		return types.Inbox{}, localizeCb, err
   318  	}
   319  
   320  	res, err = filterConvLocals(res, rquery, query, tlfInfo)
   321  	if err != nil {
   322  		return types.Inbox{}, localizeCb, err
   323  	}
   324  
   325  	return types.Inbox{
   326  		Version:         inbox.Version,
   327  		Convs:           res,
   328  		ConvsUnverified: inbox.ConvsUnverified,
   329  	}, localizeCb, nil
   330  }
   331  
   332  func (s *RemoteInboxSource) ReadUnverified(ctx context.Context, uid gregor1.UID,
   333  	dataSource types.InboxSourceDataSourceTyp, rquery *chat1.GetInboxQuery) (types.Inbox, error) {
   334  	if s.IsOffline(ctx) {
   335  		return types.Inbox{}, OfflineError{}
   336  	}
   337  	ib, err := s.getChatInterface().GetInboxRemote(ctx, chat1.GetInboxRemoteArg{
   338  		Query: s.setDefaultParticipantMode(rquery),
   339  	})
   340  	if err != nil {
   341  		return types.Inbox{}, err
   342  	}
   343  	return types.Inbox{
   344  		Version:         ib.Inbox.Full().Vers,
   345  		ConvsUnverified: utils.RemoteConvs(ib.Inbox.Full().Conversations),
   346  	}, nil
   347  }
   348  
   349  func (s *RemoteInboxSource) MarkAsRead(ctx context.Context, convID chat1.ConversationID,
   350  	uid gregor1.UID, msgID *chat1.MessageID, forceUnread bool) (err error) {
   351  	defer s.Trace(ctx, &err, "MarkAsRead(%s,%v,%v)", convID, msgID, forceUnread)()
   352  	if msgID == nil {
   353  		conv, err := utils.GetUnverifiedConv(ctx, s.G(), uid, convID, types.InboxSourceDataSourceAll)
   354  		if err != nil {
   355  			return err
   356  		}
   357  		msgID = new(chat1.MessageID)
   358  		*msgID = conv.Conv.ReaderInfo.MaxMsgid
   359  	}
   360  	if _, err = s.getChatInterface().MarkAsRead(ctx, chat1.MarkAsReadArg{
   361  		ConversationID: convID,
   362  		MsgID:          *msgID,
   363  		ForceUnread:    forceUnread,
   364  	}); err != nil {
   365  		return err
   366  	}
   367  	return nil
   368  }
   369  
   370  func (s *RemoteInboxSource) Search(ctx context.Context, uid gregor1.UID, query string, limit int,
   371  	emptyMode types.InboxSourceSearchEmptyMode) (res []types.RemoteConversation, err error) {
   372  	return nil, errors.New("not implemented")
   373  }
   374  
   375  func (s *RemoteInboxSource) IsTeam(ctx context.Context, uid gregor1.UID, item string) (bool, error) {
   376  	return false, errors.New("not implemented")
   377  }
   378  
   379  func (s *RemoteInboxSource) NewConversation(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
   380  	conv chat1.Conversation) error {
   381  	return nil
   382  }
   383  
   384  func (s *RemoteInboxSource) Sync(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers, convs []chat1.Conversation) (res types.InboxSyncRes, err error) {
   385  	return res, nil
   386  }
   387  
   388  func (s *RemoteInboxSource) NewMessage(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
   389  	convID chat1.ConversationID, msg chat1.MessageBoxed, maxMsgs []chat1.MessageSummary) (*chat1.ConversationLocal, error) {
   390  	return nil, nil
   391  }
   392  
   393  func (s *RemoteInboxSource) ReadMessage(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
   394  	convID chat1.ConversationID, msgID chat1.MessageID) (*chat1.ConversationLocal, error) {
   395  	return nil, nil
   396  }
   397  
   398  func (s *RemoteInboxSource) SetStatus(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
   399  	convID chat1.ConversationID, status chat1.ConversationStatus) (*chat1.ConversationLocal, error) {
   400  	return nil, nil
   401  }
   402  
   403  func (s *RemoteInboxSource) SetAppNotificationSettings(ctx context.Context, uid gregor1.UID,
   404  	vers chat1.InboxVers, convID chat1.ConversationID, settings chat1.ConversationNotificationInfo) (*chat1.ConversationLocal, error) {
   405  	return nil, nil
   406  }
   407  
   408  func (s *RemoteInboxSource) TlfFinalize(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
   409  	convIDs []chat1.ConversationID, finalizeInfo chat1.ConversationFinalizeInfo) ([]chat1.ConversationLocal, error) {
   410  	// Notify rest of system about reset
   411  	s.notifyTlfFinalize(ctx, finalizeInfo.ResetUser)
   412  	return nil, nil
   413  }
   414  
   415  func (s *RemoteInboxSource) MembershipUpdate(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
   416  	joined []chat1.ConversationMember, removed []chat1.ConversationMember, resets []chat1.ConversationMember,
   417  	previews []chat1.ConversationID, teamMemberRoleUpdate *chat1.TeamMemberRoleUpdate) (res types.MembershipUpdateRes, err error) {
   418  	return res, err
   419  }
   420  
   421  func (s *RemoteInboxSource) ConversationsUpdate(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
   422  	convUpdates []chat1.ConversationUpdate) error {
   423  	return nil
   424  }
   425  
   426  func (s *RemoteInboxSource) Expunge(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers, convID chat1.ConversationID,
   427  	expunge chat1.Expunge, maxMsgs []chat1.MessageSummary) (res *chat1.ConversationLocal, err error) {
   428  	return res, err
   429  }
   430  
   431  func (s *RemoteInboxSource) SetConvRetention(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
   432  	convID chat1.ConversationID, policy chat1.RetentionPolicy) (res *chat1.ConversationLocal, err error) {
   433  	return res, err
   434  }
   435  
   436  func (s *RemoteInboxSource) SetTeamRetention(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
   437  	teamID keybase1.TeamID, policy chat1.RetentionPolicy) (res []chat1.ConversationLocal, err error) {
   438  	return res, err
   439  }
   440  
   441  func (s *RemoteInboxSource) SetConvSettings(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
   442  	convID chat1.ConversationID, convSettings *chat1.ConversationSettings) (res *chat1.ConversationLocal, err error) {
   443  	return res, err
   444  }
   445  
   446  func (s *RemoteInboxSource) SubteamRename(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
   447  	convIDs []chat1.ConversationID) (convs []chat1.ConversationLocal, err error) {
   448  	return convs, err
   449  }
   450  
   451  func (s *RemoteInboxSource) TeamTypeChanged(ctx context.Context, uid gregor1.UID,
   452  	vers chat1.InboxVers, convID chat1.ConversationID, teamType chat1.TeamType) (conv *chat1.ConversationLocal, err error) {
   453  	return conv, err
   454  }
   455  
   456  func (s *RemoteInboxSource) UpgradeKBFSToImpteam(ctx context.Context, uid gregor1.UID,
   457  	vers chat1.InboxVers, convID chat1.ConversationID) (conv *chat1.ConversationLocal, err error) {
   458  	return conv, err
   459  }
   460  
   461  func (s *RemoteInboxSource) UpdateInboxVersion(ctx context.Context, uid gregor1.UID,
   462  	vers chat1.InboxVers) error {
   463  	return nil
   464  }
   465  
   466  func (s *RemoteInboxSource) Draft(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   467  	text *string) error {
   468  	return nil
   469  }
   470  
   471  func (s *RemoteInboxSource) MergeLocalMetadata(ctx context.Context, uid gregor1.UID,
   472  	convs []chat1.ConversationLocal) error {
   473  	return nil
   474  }
   475  
   476  func (s *RemoteInboxSource) NotifyUpdate(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID) {
   477  }
   478  
   479  func (s *RemoteInboxSource) IncrementLocalConvVersion(ctx context.Context, uid gregor1.UID,
   480  	convID chat1.ConversationID) (conv *chat1.ConversationLocal, err error) {
   481  	return nil, nil
   482  }
   483  
   484  func (s *RemoteInboxSource) UpdateLocalMtime(ctx context.Context, uid gregor1.UID, updates []chat1.LocalMtimeUpdate) error {
   485  	return nil
   486  }
   487  
   488  func (s *RemoteInboxSource) TeamBotSettingsForConv(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID) (
   489  	map[keybase1.UID]keybase1.TeamBotSettings, error) {
   490  	return nil, nil
   491  }
   492  
   493  type HybridInboxSource struct {
   494  	sync.Mutex
   495  	globals.Contextified
   496  	utils.DebugLabeler
   497  	*baseInboxSource
   498  
   499  	uid                   gregor1.UID
   500  	started               bool
   501  	stopCh                chan struct{}
   502  	eg                    errgroup.Group
   503  	readOutbox            *storage.ReadOutbox
   504  	readFlushDelay        time.Duration
   505  	readFlushCh           chan struct{}
   506  	searchStatusMap       map[chat1.ConversationStatus]bool
   507  	searchMemberStatusMap map[chat1.ConversationMemberStatus]bool
   508  }
   509  
   510  var _ types.InboxSource = (*HybridInboxSource)(nil)
   511  
   512  func NewHybridInboxSource(g *globals.Context,
   513  	getChatInterface func() chat1.RemoteInterface) *HybridInboxSource {
   514  	labeler := utils.NewDebugLabeler(g.ExternalG(), "HybridInboxSource", false)
   515  	s := &HybridInboxSource{
   516  		Contextified:   globals.NewContextified(g),
   517  		DebugLabeler:   labeler,
   518  		readFlushDelay: 5 * time.Second,
   519  		readFlushCh:    make(chan struct{}, 10),
   520  	}
   521  	s.searchStatusMap = map[chat1.ConversationStatus]bool{
   522  		chat1.ConversationStatus_UNFILED:  true,
   523  		chat1.ConversationStatus_FAVORITE: true,
   524  		chat1.ConversationStatus_MUTED:    true,
   525  		chat1.ConversationStatus_IGNORED:  true,
   526  	}
   527  	s.searchMemberStatusMap = map[chat1.ConversationMemberStatus]bool{
   528  		chat1.ConversationMemberStatus_ACTIVE:  true,
   529  		chat1.ConversationMemberStatus_PREVIEW: true,
   530  		chat1.ConversationMemberStatus_RESET:   true,
   531  	}
   532  	s.baseInboxSource = newBaseInboxSource(g, s, getChatInterface)
   533  	return s
   534  }
   535  
   536  func (s *HybridInboxSource) maybeNuke(ctx context.Context, uid gregor1.UID, convID *chat1.ConversationID, err *error) {
   537  	if err != nil && utils.IsDeletedConvError(*err) {
   538  		s.Debug(ctx, "purging caches on: %v for convID: %v, uid: %v", *err, convID, uid)
   539  		if ierr := s.G().InboxSource.Clear(ctx, uid, &types.ClearOpts{
   540  			SendLocalAdminNotification: true,
   541  			Reason:                     "Got unexpected conversation deleted error. Cleared conv and inbox cache",
   542  		}); ierr != nil {
   543  			s.Debug(ctx, "unable to Clear inbox: %v", ierr)
   544  		}
   545  		if convID != nil {
   546  			if ierr := s.G().ConvSource.Clear(ctx, *convID, uid, nil); ierr != nil {
   547  				s.Debug(ctx, "unable to Clear conv: %v", ierr)
   548  			}
   549  		}
   550  		s.G().UIInboxLoader.UpdateLayout(ctx, chat1.InboxLayoutReselectMode_DEFAULT, "InboxSource#maybeNuke")
   551  		*err = nil
   552  	}
   553  }
   554  
   555  func (s *HybridInboxSource) createInbox() *storage.Inbox {
   556  	return storage.NewInbox(s.G(),
   557  		storage.LayoutChangedNotifier(s.G().UIInboxLoader))
   558  }
   559  
   560  func (s *HybridInboxSource) Clear(ctx context.Context, uid gregor1.UID, opts *types.ClearOpts) (err error) {
   561  	defer s.Trace(ctx, &err, "Clear(%v)", uid)()
   562  	defer s.PerfTrace(ctx, &err, "Clear(%v)", uid)()
   563  	start := time.Now()
   564  	defer func() {
   565  		var message string
   566  		if err == nil {
   567  			message = fmt.Sprintf("Clearing inbox for %s", uid)
   568  		} else {
   569  			message = fmt.Sprintf("Failed to clear inbox %s", uid)
   570  		}
   571  		s.G().RuntimeStats.PushPerfEvent(keybase1.PerfEvent{
   572  			EventType: keybase1.PerfEventType_CLEARINBOX,
   573  			Message:   message,
   574  			Ctime:     keybase1.ToTime(start),
   575  		})
   576  	}()
   577  	kuid := keybase1.UID(uid.String())
   578  	if (s.G().Env.GetRunMode() == libkb.DevelRunMode || libkb.IsKeybaseAdmin(kuid)) &&
   579  		s.G().UIRouter != nil && opts != nil && opts.SendLocalAdminNotification {
   580  		ui, err := s.G().UIRouter.GetLogUI()
   581  		if err == nil && ui != nil {
   582  			ui.Critical("Clearing inbox: %s", opts.Reason)
   583  		}
   584  	}
   585  
   586  	return s.createInbox().Clear(ctx, uid)
   587  }
   588  
   589  func (s *HybridInboxSource) Connected(ctx context.Context) {
   590  	defer s.Trace(ctx, nil, "Connected")()
   591  	s.baseInboxSource.Connected(ctx)
   592  	s.flushMarkAsRead(ctx)
   593  }
   594  
   595  func (s *HybridInboxSource) Start(ctx context.Context, uid gregor1.UID) {
   596  	defer s.Trace(ctx, nil, "Start")()
   597  	s.baseInboxSource.Start(ctx, uid)
   598  	s.Lock()
   599  	defer s.Unlock()
   600  	if s.started {
   601  		return
   602  	}
   603  	s.stopCh = make(chan struct{})
   604  	s.started = true
   605  	s.uid = uid
   606  	s.readOutbox = storage.NewReadOutbox(s.G(), uid)
   607  	s.eg.Go(func() error { return s.markAsReadDeliverLoop(uid, s.stopCh) })
   608  }
   609  
   610  func (s *HybridInboxSource) Stop(ctx context.Context) chan struct{} {
   611  	defer s.Trace(ctx, nil, "Stop")()
   612  	<-s.baseInboxSource.Stop(ctx)
   613  	s.Lock()
   614  	defer s.Unlock()
   615  	ch := make(chan struct{})
   616  	if s.started {
   617  		close(s.stopCh)
   618  		s.started = false
   619  		go func() {
   620  			_ = s.eg.Wait()
   621  			close(ch)
   622  		}()
   623  	} else {
   624  		close(ch)
   625  	}
   626  	return ch
   627  }
   628  
   629  func (s *HybridInboxSource) flushMarkAsRead(ctx context.Context) {
   630  	select {
   631  	case s.readFlushCh <- struct{}{}:
   632  	default:
   633  		s.Debug(ctx, "flushMarkAsRead: channel full, dropping")
   634  	}
   635  }
   636  
   637  func (s *HybridInboxSource) markAsReadDeliver(ctx context.Context) (err error) {
   638  	defer func() {
   639  		if err != nil {
   640  			s.Debug(ctx, "markAsReadDeliver: failed to mark as read: %s", err)
   641  		}
   642  	}()
   643  	recs, err := s.readOutbox.GetRecords(ctx)
   644  	if err != nil {
   645  		return err
   646  	}
   647  	for _, rec := range recs {
   648  		shouldRemove := false
   649  		if _, err := s.getChatInterface().MarkAsRead(ctx, chat1.MarkAsReadArg{
   650  			ConversationID: rec.ConvID,
   651  			MsgID:          rec.MsgID,
   652  			ForceUnread:    rec.ForceUnread,
   653  		}); err != nil {
   654  			s.Debug(ctx, "markAsReadDeliver: failed to mark as read: convID: %s msgID: %s forceUnread: %v err: %s",
   655  				rec.ConvID, rec.MsgID, rec.ForceUnread, err)
   656  			// check for an immediate failure from the server, and get the attempt out if it fails
   657  			if berr, ok := err.(DelivererInfoError); ok {
   658  				if _, ok := berr.IsImmediateFail(); ok {
   659  					s.Debug(ctx, "markAsReadDeliver: error is an immediate failure, not retrying")
   660  					shouldRemove = true
   661  				}
   662  			}
   663  		} else {
   664  			shouldRemove = true
   665  		}
   666  		if shouldRemove {
   667  			if err := s.readOutbox.RemoveRecord(ctx, rec.ID); err != nil {
   668  				s.Debug(ctx, "markAsReadDeliver: failed to remove record: %s", err)
   669  			}
   670  		}
   671  	}
   672  	return nil
   673  }
   674  
   675  func (s *HybridInboxSource) markAsReadDeliverLoop(uid gregor1.UID, stopCh chan struct{}) error {
   676  	ctx := context.Background()
   677  	for {
   678  		select {
   679  		case <-s.readFlushCh:
   680  			if err := s.markAsReadDeliver(ctx); err != nil {
   681  				s.Debug(ctx, "unable to mark as read: %v", err)
   682  			}
   683  		case <-s.G().Clock().After(s.readFlushDelay):
   684  			if err := s.markAsReadDeliver(ctx); err != nil {
   685  				s.Debug(ctx, "unable to mark as read: %v", err)
   686  			}
   687  		case <-stopCh:
   688  			return nil
   689  		}
   690  	}
   691  }
   692  
   693  func makeBadgeConversationInfo(convID keybase1.ChatConversationID, count int) keybase1.BadgeConversationInfo {
   694  	return keybase1.BadgeConversationInfo{
   695  		ConvID:         convID,
   696  		BadgeCount:     count,
   697  		UnreadMessages: count,
   698  	}
   699  }
   700  
   701  // ApplyLocalChatState marks items locally as read and badges conversations
   702  // that have failed outbox items.
   703  func (s *HybridInboxSource) ApplyLocalChatState(ctx context.Context, infos []keybase1.BadgeConversationInfo) (res []keybase1.BadgeConversationInfo, smallTeamBadgeCount, bigTeamBadgeCount int) {
   704  	convIDs := make([]chat1.ConversationID, 0, len(infos))
   705  	for _, info := range infos {
   706  		if !info.IsEmpty() {
   707  			convIDs = append(convIDs, chat1.ConversationID(info.ConvID.Bytes()))
   708  		}
   709  	}
   710  
   711  	outbox := storage.NewOutbox(s.G(), s.uid)
   712  	obrs, oerr := outbox.PullAllConversations(ctx, true /*includeErrors */, false /*remove*/)
   713  	if oerr != nil {
   714  		s.Debug(ctx, "ApplyLocalChatState: failed to get outbox: %v", oerr)
   715  	}
   716  
   717  	failedOutboxMap := make(map[chat1.ConvIDStr]int)
   718  	localUpdates := make(map[chat1.ConvIDStr]chat1.LocalMtimeUpdate)
   719  	s.Debug(ctx, "ApplyLocalChatState: looking through %d outbox items for badgable errors", len(obrs))
   720  	for _, obr := range obrs {
   721  		if !(obr.IsBadgable() && obr.IsError()) {
   722  			s.Debug(ctx, "ApplyLocalChatState: skipping msgTyp: %v", obr.Msg.MessageType())
   723  			continue
   724  		}
   725  		ctime := obr.Ctime
   726  		isBadgableError := obr.State.Error().Typ.IsBadgableError()
   727  		s.Debug(ctx, "ApplyLocalChatState: found erred outbox item ctime: %v, error: %v, messageType: %v, IsBadgableError: %v",
   728  			ctime.Time(), obr.State.Error(), obr.Msg.MessageType(), isBadgableError)
   729  		if !isBadgableError {
   730  			continue
   731  		}
   732  		convIDStr := obr.ConvID.ConvIDStr()
   733  		if update, ok := localUpdates[convIDStr]; ok {
   734  			if ctime.After(update.Mtime) {
   735  				localUpdates[convIDStr] = update
   736  			}
   737  		} else {
   738  			localUpdates[convIDStr] = chat1.LocalMtimeUpdate{
   739  				ConvID: obr.ConvID,
   740  				Mtime:  ctime,
   741  			}
   742  		}
   743  		convIDs = append(convIDs, obr.ConvID)
   744  		failedOutboxMap[convIDStr]++
   745  	}
   746  
   747  	_, convs, err := s.createInbox().Read(ctx, s.uid, &chat1.GetInboxQuery{
   748  		ConvIDs: convIDs,
   749  	})
   750  	if err != nil {
   751  		s.Debug(ctx, "ApplyLocalChatState: failed to get convs: %v, charging forward", err)
   752  	}
   753  	// convID -> isRead
   754  	readConvMap := make(map[chat1.ConvIDStr]bool)
   755  	smallTeamConvMap := make(map[chat1.ConvIDStr]bool, len(convs))
   756  	for _, conv := range convs {
   757  		if conv.IsLocallyRead() {
   758  			readConvMap[conv.ConvIDStr] = true
   759  		}
   760  		smallTeamConvMap[conv.ConvIDStr] = conv.GetTeamType() != chat1.TeamType_COMPLEX
   761  	}
   762  
   763  	updates := make([]chat1.LocalMtimeUpdate, 0, len(localUpdates))
   764  	for _, update := range localUpdates {
   765  		updates = append(updates, update)
   766  	}
   767  	if err := s.createInbox().UpdateLocalMtime(ctx, s.uid, updates); err != nil {
   768  		s.Debug(ctx, "ApplyLocalChatState: unable to apply UpdateLocalMtime: %v", err)
   769  	}
   770  
   771  	res = make([]keybase1.BadgeConversationInfo, 0, len(infos)+len(failedOutboxMap))
   772  	for _, info := range infos {
   773  		convIDStr := chat1.ConvIDStr(info.ConvID.String())
   774  		// mark this conv as read
   775  		if readConvMap[convIDStr] {
   776  			info = makeBadgeConversationInfo(info.ConvID, 0)
   777  			s.Debug(ctx, "ApplyLocalChatState, marking as read %+v", info)
   778  		}
   779  		// badge qualifying failed outbox items
   780  		if failedCount, ok := failedOutboxMap[convIDStr]; ok {
   781  			newInfo := makeBadgeConversationInfo(info.ConvID, failedCount)
   782  			newInfo.BadgeCount += info.BadgeCount
   783  			newInfo.UnreadMessages += info.UnreadMessages
   784  			info = newInfo
   785  			delete(failedOutboxMap, convIDStr)
   786  			s.Debug(ctx, "ApplyLocalChatState, applying failed to existing info %+v", info)
   787  		}
   788  		if isSmallTeam, ok := smallTeamConvMap[convIDStr]; ok {
   789  			if isSmallTeam {
   790  				smallTeamBadgeCount += info.BadgeCount
   791  			} else {
   792  				bigTeamBadgeCount += info.BadgeCount
   793  			}
   794  		}
   795  		res = append(res, info)
   796  	}
   797  
   798  	// apply any new failed outbox items
   799  	for convIDStr, failedCount := range failedOutboxMap {
   800  		convID, err := chat1.MakeConvID(convIDStr.String())
   801  		if err != nil {
   802  			s.Debug(ctx, "ApplyLocalChatState: Unable to make convID: %v", err)
   803  			continue
   804  		}
   805  		newInfo := makeBadgeConversationInfo(keybase1.ChatConversationID(convID), failedCount)
   806  		s.Debug(ctx, "ApplyLocalChatState, applying failed to new info %+v", newInfo)
   807  		res = append(res, newInfo)
   808  	}
   809  	return res, smallTeamBadgeCount, bigTeamBadgeCount
   810  }
   811  
   812  func (s *HybridInboxSource) Draft(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   813  	text *string) (err error) {
   814  	defer s.maybeNuke(ctx, uid, &convID, &err)
   815  	_, err = s.createInbox().Draft(ctx, uid, convID, text)
   816  	if err != nil {
   817  		return err
   818  	}
   819  	return nil
   820  }
   821  
   822  func (s *HybridInboxSource) UpdateLocalMtime(ctx context.Context, uid gregor1.UID, updates []chat1.LocalMtimeUpdate) error {
   823  	if err := s.createInbox().UpdateLocalMtime(ctx, uid, updates); err != nil {
   824  		return err
   825  	}
   826  	return nil
   827  }
   828  
   829  func (s *HybridInboxSource) MergeLocalMetadata(ctx context.Context, uid gregor1.UID, convs []chat1.ConversationLocal) (err error) {
   830  	defer s.Trace(ctx, &err, "MergeLocalMetadata")()
   831  	defer s.maybeNuke(ctx, uid, nil, &err)
   832  	return s.createInbox().MergeLocalMetadata(ctx, uid, convs)
   833  }
   834  
   835  func (s *HybridInboxSource) NotifyUpdate(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID) {
   836  	if err := s.createInbox().IncrementLocalConvVersion(ctx, uid, convID); err != nil {
   837  		s.Debug(ctx, "NotifyUpdate: unable to IncrementLocalConvVersion, err", err)
   838  	}
   839  	conv, err := s.getConvLocal(ctx, uid, convID)
   840  	if err != nil {
   841  		s.Debug(ctx, "NotifyUpdate: unable to getConvLocal, err", err)
   842  	}
   843  	var inboxUIItem *chat1.InboxUIItem
   844  	topicType := chat1.TopicType_NONE
   845  	if conv != nil {
   846  		inboxUIItem = PresentConversationLocalWithFetchRetry(ctx, s.G(), uid, *conv,
   847  			utils.PresentParticipantsModeSkip)
   848  		topicType = conv.GetTopicType()
   849  	}
   850  	s.G().ActivityNotifier.ConvUpdate(ctx, uid, convID,
   851  		topicType, inboxUIItem)
   852  }
   853  
   854  func (s *HybridInboxSource) IncrementLocalConvVersion(ctx context.Context, uid gregor1.UID,
   855  	convID chat1.ConversationID) (conv *chat1.ConversationLocal, err error) {
   856  	defer s.Trace(ctx, &err, "IncrementLocalConvVersion")()
   857  	defer s.maybeNuke(ctx, uid, &convID, &err)
   858  	if err := s.createInbox().IncrementLocalConvVersion(ctx, uid, convID); err != nil {
   859  		s.Debug(ctx, "IncrementLocalConvVersion: unable to IncrementLocalConvVersion, err", err)
   860  	}
   861  	return s.getConvLocal(ctx, uid, convID)
   862  }
   863  
   864  func (s *HybridInboxSource) MarkAsRead(ctx context.Context, convID chat1.ConversationID,
   865  	uid gregor1.UID, msgID *chat1.MessageID, forceUnread bool) (err error) {
   866  	defer s.Trace(ctx, &err, "MarkAsRead(%s,%d, %v)", convID, msgID, forceUnread)()
   867  	defer s.maybeNuke(ctx, uid, &convID, &err)
   868  	if !forceUnread {
   869  		// Check local copy to see if we have this convo, and have fully read
   870  		// it. If so, we skip the remote call unless
   871  		readRes, err := s.createInbox().GetConversation(ctx, uid, convID)
   872  		if err == nil && readRes.GetConvID().Eq(convID) &&
   873  			readRes.Conv.ReaderInfo.ReadMsgid == readRes.Conv.ReaderInfo.MaxMsgid {
   874  			s.Debug(ctx, "MarkAsRead: conversation fully read: %s, not sending remote call", convID)
   875  			return nil
   876  		}
   877  	}
   878  	if msgID == nil {
   879  		conv, err := utils.GetUnverifiedConv(ctx, s.G(), uid, convID, types.InboxSourceDataSourceAll)
   880  		if err != nil {
   881  			return err
   882  		}
   883  		msgID = new(chat1.MessageID)
   884  		*msgID = conv.Conv.ReaderInfo.MaxMsgid
   885  	}
   886  	if err := s.createInbox().MarkLocalRead(ctx, uid, convID, *msgID); err != nil {
   887  		s.Debug(ctx, "MarkAsRead: failed to mark local read: %s", err)
   888  	} else {
   889  		if err := s.G().Badger.Send(ctx); err != nil {
   890  			return err
   891  		}
   892  	}
   893  	if err := s.readOutbox.PushRead(ctx, convID, *msgID, forceUnread); err != nil {
   894  		return err
   895  	}
   896  	s.flushMarkAsRead(ctx)
   897  	return nil
   898  }
   899  
   900  func (s *HybridInboxSource) fetchRemoteInbox(ctx context.Context, uid gregor1.UID,
   901  	query *chat1.GetInboxQuery) (res types.Inbox, err error) {
   902  	defer s.Trace(ctx, &err, "fetchRemoteInbox")()
   903  
   904  	// Insta fail if we are offline
   905  	if s.IsOffline(ctx) {
   906  		return types.Inbox{}, OfflineError{}
   907  	}
   908  
   909  	// We always want this on for fetches to fill the local inbox, otherwise we never get the
   910  	// full list for the conversations that come back
   911  	var rquery chat1.GetInboxQuery
   912  	if query == nil {
   913  		rquery = chat1.GetInboxQuery{
   914  			ComputeActiveList: true,
   915  		}
   916  	} else {
   917  		rquery = *query
   918  		rquery.ComputeActiveList = true
   919  	}
   920  	rquery.SummarizeMaxMsgs = true // always summarize max msgs
   921  
   922  	ib, err := s.getChatInterface().GetInboxRemote(ctx, chat1.GetInboxRemoteArg{
   923  		Query: s.setDefaultParticipantMode(&rquery),
   924  	})
   925  	if err != nil {
   926  		return types.Inbox{}, err
   927  	}
   928  
   929  	var bgEnqueued int
   930  	// Limit the number of jobs we enqueue when on a limited data connection in
   931  	// mobile.
   932  	maxBgEnqueued := 10
   933  	if s.G().MobileNetState.State().IsLimited() {
   934  		maxBgEnqueued = 3
   935  	}
   936  
   937  	for _, conv := range ib.Inbox.Full().Conversations {
   938  		// Retention policy expunge
   939  		expunge := conv.GetExpunge()
   940  		if expunge != nil {
   941  			err := s.G().ConvSource.Expunge(ctx, utils.RemoteConv(conv), uid, *expunge)
   942  			if err != nil {
   943  				return types.Inbox{}, err
   944  			}
   945  		}
   946  		if query != nil && query.SkipBgLoads {
   947  			continue
   948  		}
   949  		// Queue all these convs up to be loaded by the background loader. Only
   950  		// load first maxBgEnqueued non KBFS convs, ACTIVE convs so we don't
   951  		// get the conv loader too backed up.
   952  		if conv.Metadata.MembersType != chat1.ConversationMembersType_KBFS &&
   953  			(conv.HasMemberStatus(chat1.ConversationMemberStatus_ACTIVE) ||
   954  				conv.HasMemberStatus(chat1.ConversationMemberStatus_PREVIEW)) &&
   955  			bgEnqueued < maxBgEnqueued {
   956  			job := types.NewConvLoaderJob(conv.GetConvID(), &chat1.Pagination{Num: 50},
   957  				types.ConvLoaderPriorityMedium, types.ConvLoaderGeneric, nil)
   958  			if err := s.G().ConvLoader.Queue(ctx, job); err != nil {
   959  				s.Debug(ctx, "fetchRemoteInbox: failed to queue conversation load: %s", err)
   960  			}
   961  			bgEnqueued++
   962  		}
   963  	}
   964  	convs := utils.RemoteConvs(ib.Inbox.Full().Conversations)
   965  	convs = utils.ApplyInboxQuery(ctx, s.DebugLabeler, query, convs)
   966  	return types.Inbox{
   967  		Version:         ib.Inbox.Full().Vers,
   968  		ConvsUnverified: convs,
   969  	}, nil
   970  }
   971  
   972  func (s *HybridInboxSource) Read(ctx context.Context, uid gregor1.UID,
   973  	localizerTyp types.ConversationLocalizerTyp, dataSource types.InboxSourceDataSourceTyp, maxLocalize *int,
   974  	query *chat1.GetInboxLocalQuery) (inbox types.Inbox, localizeCb chan types.AsyncInboxResult, err error) {
   975  	defer s.Trace(ctx, &err, "Read")()
   976  	defer s.maybeNuke(ctx, uid, nil, &err)
   977  
   978  	// Read unverified inbox
   979  	rquery, tlfInfo, err := s.GetInboxQueryLocalToRemote(ctx, query)
   980  	if err != nil {
   981  		return inbox, localizeCb, err
   982  	}
   983  	inbox, err = s.ReadUnverified(ctx, uid, dataSource, rquery)
   984  	if err != nil {
   985  		return inbox, localizeCb, err
   986  	}
   987  	// we add an additional 1 here for the unverified payload which is also sent
   988  	// on this channel
   989  	localizeCb = make(chan types.AsyncInboxResult, len(inbox.ConvsUnverified)+1)
   990  	localizer := s.createConversationLocalizer(ctx, localizerTyp, localizeCb)
   991  	s.Debug(ctx, "Read: using localizer: %s on %d convs", localizer.Name(), len(inbox.ConvsUnverified))
   992  
   993  	// Localize
   994  	inbox.Convs, err = localizer.Localize(ctx, uid, inbox, maxLocalize)
   995  	if err != nil {
   996  		return inbox, localizeCb, err
   997  	}
   998  
   999  	// Run post filters
  1000  	inbox.Convs, err = filterConvLocals(inbox.Convs, rquery, query, tlfInfo)
  1001  	if err != nil {
  1002  		return inbox, localizeCb, err
  1003  	}
  1004  
  1005  	// Write metadata to the inbox cache
  1006  	if err = s.createInbox().MergeLocalMetadata(ctx, uid, inbox.Convs); err != nil {
  1007  		// Don't abort the operation on this kind of error
  1008  		s.Debug(ctx, "Read: unable to write inbox local metadata: %s", err)
  1009  	}
  1010  
  1011  	return inbox, localizeCb, nil
  1012  }
  1013  
  1014  func (s *HybridInboxSource) ReadUnverified(ctx context.Context, uid gregor1.UID,
  1015  	dataSource types.InboxSourceDataSourceTyp, query *chat1.GetInboxQuery) (res types.Inbox, err error) {
  1016  	defer s.Trace(ctx, &err, "ReadUnverified")()
  1017  	defer s.maybeNuke(ctx, uid, nil, &err)
  1018  
  1019  	var cerr storage.Error
  1020  	inboxStore := s.createInbox()
  1021  	mergeInboxStore := false
  1022  
  1023  	// Try local storage (if enabled)
  1024  	switch dataSource {
  1025  	case types.InboxSourceDataSourceLocalOnly, types.InboxSourceDataSourceAll:
  1026  		var vers chat1.InboxVers
  1027  		var convs []types.RemoteConversation
  1028  		mergeInboxStore = true
  1029  		vers, convs, cerr = inboxStore.Read(ctx, uid, query)
  1030  		if cerr == nil {
  1031  			s.Debug(ctx, "ReadUnverified: hit local storage: uid: %s convs: %d", uid, len(convs))
  1032  			res = types.Inbox{
  1033  				Version:         vers,
  1034  				ConvsUnverified: convs,
  1035  			}
  1036  		} else {
  1037  			if dataSource == types.InboxSourceDataSourceLocalOnly {
  1038  				s.Debug(ctx, "ReadUnverified: missed local storage, and in local only mode: %s", cerr)
  1039  				return res, cerr
  1040  			}
  1041  		}
  1042  	default:
  1043  		cerr = storage.MissError{}
  1044  	}
  1045  
  1046  	// If we hit an error reading from storage, then read from remote
  1047  	if cerr != nil {
  1048  		if _, ok := cerr.(storage.MissError); !ok {
  1049  			s.Debug(ctx, "ReadUnverified: error fetching inbox: %s", cerr.Error())
  1050  		} else {
  1051  			s.Debug(ctx, "ReadUnverified: storage miss")
  1052  		}
  1053  
  1054  		// Go to the remote on miss
  1055  		res, err = s.fetchRemoteInbox(ctx, uid, query)
  1056  		if err != nil {
  1057  			return res, err
  1058  		}
  1059  
  1060  		// Write out to local storage only if we are using local data
  1061  		if mergeInboxStore {
  1062  			if cerr = inboxStore.Merge(ctx, uid, res.Version, utils.PluckConvs(res.ConvsUnverified), query); cerr != nil {
  1063  				s.Debug(ctx, "ReadUnverified: failed to write inbox to local storage: %s", cerr.Error())
  1064  			}
  1065  		}
  1066  	}
  1067  
  1068  	return res, err
  1069  }
  1070  
  1071  type nameContainsQueryRes int
  1072  
  1073  const (
  1074  	nameContainsQueryNone nameContainsQueryRes = iota
  1075  	nameContainsQuerySimilar
  1076  	nameContainsQueryPrefix
  1077  	fullNameContainsQueryExact
  1078  	nameContainsQueryExact
  1079  	nameContainsQueryUnread
  1080  	nameContainsQueryBadged
  1081  )
  1082  
  1083  type convSearchHit struct {
  1084  	conv      types.RemoteConversation
  1085  	queryToks []string
  1086  	convToks  []string
  1087  	nameToks  []string
  1088  	hits      []nameContainsQueryRes
  1089  }
  1090  
  1091  // weight contacts in the past week
  1092  const (
  1093  	lastActiveWeight   = 50.0
  1094  	lastActiveMinHours = 24     // time in the last day yields max score
  1095  	lastActiveMaxHours = 7 * 24 // time greater than a week yields min score
  1096  )
  1097  
  1098  func (h convSearchHit) score(emptyMode types.InboxSourceSearchEmptyMode) (score float64) {
  1099  	exactNames := 0
  1100  	for _, hit := range h.hits {
  1101  		switch hit {
  1102  		case nameContainsQueryExact:
  1103  			score += 20
  1104  			exactNames++
  1105  		case fullNameContainsQueryExact:
  1106  			score += 50
  1107  		case nameContainsQueryPrefix:
  1108  			score += 10
  1109  		case nameContainsQuerySimilar:
  1110  			score += 3
  1111  		case nameContainsQueryUnread:
  1112  			score += 100
  1113  		case nameContainsQueryBadged:
  1114  			score += 200
  1115  		}
  1116  	}
  1117  	if len(h.queryToks) == len(h.convToks) && exactNames >= len(h.convToks) {
  1118  		score += 1000000
  1119  	}
  1120  
  1121  	var htime gregor1.Time
  1122  	switch emptyMode {
  1123  	case types.InboxSourceSearchEmptyModeAllBySendCtime:
  1124  		htime = utils.GetConvLastSendTime(h.conv)
  1125  	default:
  1126  		htime = utils.GetConvMtime(h.conv)
  1127  	}
  1128  
  1129  	lastActiveScore := opensearch.NormalizeLastActive(lastActiveMinHours, lastActiveMaxHours, keybase1.Time(htime))
  1130  	score += lastActiveScore * lastActiveWeight
  1131  	return score
  1132  }
  1133  
  1134  func (h convSearchHit) less(o convSearchHit, emptyMode types.InboxSourceSearchEmptyMode) bool {
  1135  	hScore := h.score(emptyMode)
  1136  	oScore := o.score(emptyMode)
  1137  	if hScore < oScore {
  1138  		return true
  1139  	} else if hScore > oScore {
  1140  		return false
  1141  	}
  1142  	var htime, otime gregor1.Time
  1143  	switch emptyMode {
  1144  	case types.InboxSourceSearchEmptyModeAllBySendCtime:
  1145  		htime = utils.GetConvLastSendTime(h.conv)
  1146  		otime = utils.GetConvLastSendTime(o.conv)
  1147  	default:
  1148  		htime = utils.GetConvMtime(h.conv)
  1149  		otime = utils.GetConvMtime(o.conv)
  1150  	}
  1151  	return htime.Before(otime)
  1152  }
  1153  
  1154  func (h convSearchHit) valid() bool {
  1155  	return len(h.hits) > 0
  1156  }
  1157  
  1158  func (s *HybridInboxSource) fullNamesForSearch(ctx context.Context, conv types.RemoteConversation,
  1159  	convName, username string) (res []string) {
  1160  	switch conv.GetMembersType() {
  1161  	case chat1.ConversationMembersType_TEAM:
  1162  		return nil
  1163  	default:
  1164  	}
  1165  	if conv.LocalMetadata == nil {
  1166  		return nil
  1167  	}
  1168  	for index, name := range conv.LocalMetadata.FullNamesForSearch {
  1169  		if name == nil {
  1170  			continue
  1171  		}
  1172  		if index >= len(conv.LocalMetadata.WriterNames) {
  1173  			continue
  1174  		}
  1175  		if conv.LocalMetadata.WriterNames[index] == username && convName != username {
  1176  			continue
  1177  		}
  1178  		res = append(res, strings.Split(strings.ToLower(*name), " ")...)
  1179  	}
  1180  	return res
  1181  }
  1182  
  1183  func (s *HybridInboxSource) isConvSearchHit(ctx context.Context, conv types.RemoteConversation,
  1184  	queryToks []string, username string, emptyMode types.InboxSourceSearchEmptyMode) (res convSearchHit) {
  1185  	var convToks []string
  1186  	res.conv = conv
  1187  	res.queryToks = queryToks
  1188  	if len(queryToks) == 0 {
  1189  		switch emptyMode {
  1190  		case types.InboxSourceSearchEmptyModeUnread:
  1191  			if conv.Conv.IsUnread() {
  1192  				cqe := nameContainsQueryUnread
  1193  				if s.G().Badger.State().ConversationBadge(ctx, conv.GetConvID()) > 0 {
  1194  					cqe = nameContainsQueryBadged
  1195  				}
  1196  				res.hits = []nameContainsQueryRes{cqe}
  1197  			}
  1198  		default:
  1199  			res.hits = []nameContainsQueryRes{nameContainsQueryExact}
  1200  		}
  1201  		return res
  1202  	}
  1203  	convName := utils.SearchableRemoteConversationName(conv, username)
  1204  	switch conv.GetMembersType() {
  1205  	case chat1.ConversationMembersType_TEAM:
  1206  		convToks = []string{convName}
  1207  	default:
  1208  		convToks = strings.Split(convName, ",")
  1209  	}
  1210  	res.convToks = convToks
  1211  	res.nameToks = s.fullNamesForSearch(ctx, conv, convName, username)
  1212  	for _, queryTok := range queryToks {
  1213  		curHit := nameContainsQueryNone
  1214  		for i, convTok := range append(convToks, res.nameToks...) {
  1215  			if nameContainsQueryExact > curHit && convTok == queryTok {
  1216  				if i < len(res.convToks) {
  1217  					curHit = nameContainsQueryExact
  1218  				} else { // full name matches are slightly lower than name matches
  1219  					curHit = fullNameContainsQueryExact
  1220  				}
  1221  			} else if nameContainsQueryPrefix > curHit && strings.HasPrefix(convTok, queryTok) {
  1222  				curHit = nameContainsQueryPrefix
  1223  			} else if nameContainsQuerySimilar > curHit && strings.Contains(convTok, queryTok) {
  1224  				curHit = nameContainsQuerySimilar
  1225  			}
  1226  		}
  1227  		if curHit > nameContainsQueryNone {
  1228  			res.hits = append(res.hits, curHit)
  1229  		}
  1230  	}
  1231  	return res
  1232  }
  1233  
  1234  func (s *HybridInboxSource) Search(ctx context.Context, uid gregor1.UID, query string, limit int,
  1235  	emptyMode types.InboxSourceSearchEmptyMode) (res []types.RemoteConversation, err error) {
  1236  	defer s.Trace(ctx, &err, "Search")()
  1237  	defer s.maybeNuke(ctx, uid, nil, &err)
  1238  	username := s.G().GetEnv().GetUsernameForUID(keybase1.UID(uid.String())).String()
  1239  	ib := s.createInbox()
  1240  	_, convs, err := ib.ReadAll(ctx, uid, true)
  1241  	if err != nil {
  1242  		return res, err
  1243  	}
  1244  	// normalize the search query to lowercase
  1245  	query = strings.ToLower(query)
  1246  	var queryToks []string
  1247  	for _, t := range strings.FieldsFunc(query, func(r rune) bool {
  1248  		return r == ',' || r == ' '
  1249  	}) {
  1250  		tok := strings.Trim(t, " ")
  1251  		if len(tok) > 0 {
  1252  			queryToks = append(queryToks, tok)
  1253  		}
  1254  	}
  1255  	var hits []convSearchHit
  1256  	for _, conv := range convs {
  1257  		if conv.Conv.GetTopicType() != chat1.TopicType_CHAT ||
  1258  			utils.IsConvEmpty(conv.Conv) || conv.Conv.IsPublic() ||
  1259  			!s.searchStatusMap[conv.Conv.Metadata.Status] ||
  1260  			!s.searchMemberStatusMap[conv.Conv.ReaderInfo.Status] {
  1261  			continue
  1262  		}
  1263  		hit := s.isConvSearchHit(ctx, conv, queryToks, username, emptyMode)
  1264  		if !hit.valid() {
  1265  			continue
  1266  		}
  1267  		hits = append(hits, hit)
  1268  	}
  1269  
  1270  	sort.Slice(hits, func(i, j int) bool {
  1271  		return hits[j].less(hits[i], emptyMode)
  1272  	})
  1273  	res = make([]types.RemoteConversation, len(hits))
  1274  	for i, hit := range hits {
  1275  		res[i] = hit.conv
  1276  	}
  1277  	if limit > 0 && limit < len(res) {
  1278  		res = res[:limit]
  1279  	}
  1280  	return res, nil
  1281  }
  1282  
  1283  func (s *HybridInboxSource) IsTeam(ctx context.Context, uid gregor1.UID, item string) (res bool, err error) {
  1284  	defer s.Trace(ctx, &err, "IsTeam")()
  1285  	_, convs, err := s.createInbox().ReadAll(ctx, uid, true)
  1286  	if err != nil {
  1287  		return res, err
  1288  	}
  1289  	for _, conv := range convs {
  1290  		if conv.GetMembersType() == chat1.ConversationMembersType_TEAM &&
  1291  			utils.GetRemoteConvTLFName(conv) == item {
  1292  			return true, nil
  1293  		}
  1294  	}
  1295  	return false, nil
  1296  }
  1297  
  1298  func (s *HybridInboxSource) handleInboxError(ctx context.Context, err error, uid gregor1.UID) (ferr error) {
  1299  	defer func() {
  1300  		if ferr != nil {
  1301  			// Only do this aggressive clear if the error we get is not some kind of network error
  1302  			_, isStorageAbort := ferr.(storage.AbortedError)
  1303  			if ferr != context.Canceled && !isStorageAbort &&
  1304  				IsOfflineError(ferr) == OfflineErrorKindOnline {
  1305  				s.Debug(ctx, "handleInboxError: failed to recover from inbox error, clearing: %s", ferr)
  1306  				err := s.createInbox().Clear(ctx, uid)
  1307  				if err != nil {
  1308  					s.Debug(ctx, "handleInboxError: error clearing inbox: %+v", err)
  1309  				}
  1310  			} else {
  1311  				s.Debug(ctx, "handleInboxError: skipping inbox clear because of offline error: %s", ferr)
  1312  			}
  1313  		}
  1314  		s.G().UIInboxLoader.UpdateLayout(ctx, chat1.InboxLayoutReselectMode_DEFAULT, "inbox error")
  1315  	}()
  1316  
  1317  	if _, ok := err.(storage.MissError); ok {
  1318  		return nil
  1319  	}
  1320  	if verr, ok := err.(storage.VersionMismatchError); ok {
  1321  		s.Debug(ctx, "handleInboxError: version mismatch, syncing and sending stale notifications: %s",
  1322  			verr.Error())
  1323  		return s.G().Syncer.Sync(ctx, s.getChatInterface(), uid, nil)
  1324  	}
  1325  	return err
  1326  }
  1327  
  1328  func (s *HybridInboxSource) NewConversation(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
  1329  	conv chat1.Conversation) (err error) {
  1330  	defer s.Trace(ctx, &err, "NewConversation")()
  1331  	if cerr := s.createInbox().NewConversation(ctx, uid, vers, conv); cerr != nil {
  1332  		err = s.handleInboxError(ctx, cerr, uid)
  1333  		return err
  1334  	}
  1335  
  1336  	return nil
  1337  }
  1338  
  1339  func (s *HybridInboxSource) getConvLocal(ctx context.Context, uid gregor1.UID,
  1340  	convID chat1.ConversationID) (conv *chat1.ConversationLocal, err error) {
  1341  	// Read back affected conversation so we can send it to the frontend
  1342  	convs, err := s.getConvsLocal(ctx, uid, []chat1.ConversationID{convID})
  1343  	if err != nil {
  1344  		return nil, err
  1345  	}
  1346  	if len(convs) == 0 {
  1347  		return nil, fmt.Errorf("unable to find conversation for new message: convID: %s", convID)
  1348  	}
  1349  	if len(convs) > 1 {
  1350  		return nil, fmt.Errorf("more than one conversation returned? convID: %s", convID)
  1351  	}
  1352  	return &convs[0], nil
  1353  }
  1354  
  1355  // Get convs. May return fewer or no conversations.
  1356  func (s *HybridInboxSource) getConvsLocal(ctx context.Context, uid gregor1.UID,
  1357  	convIDs []chat1.ConversationID) ([]chat1.ConversationLocal, error) {
  1358  	// Read back affected conversation so we can send it to the frontend
  1359  	ib, _, err := s.Read(ctx, uid, types.ConversationLocalizerBlocking, types.InboxSourceDataSourceAll, nil,
  1360  		&chat1.GetInboxLocalQuery{
  1361  			ConvIDs: convIDs,
  1362  		})
  1363  	return ib.Convs, err
  1364  }
  1365  
  1366  func (s *HybridInboxSource) Sync(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers, convs []chat1.Conversation) (res types.InboxSyncRes, err error) {
  1367  	defer s.Trace(ctx, &err, "Sync")()
  1368  	return s.createInbox().Sync(ctx, uid, vers, convs)
  1369  }
  1370  
  1371  func (s *HybridInboxSource) NewMessage(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
  1372  	convID chat1.ConversationID, msg chat1.MessageBoxed, maxMsgs []chat1.MessageSummary) (conv *chat1.ConversationLocal, err error) {
  1373  	defer s.Trace(ctx, &err, "NewMessage")()
  1374  	if cerr := s.createInbox().NewMessage(ctx, uid, vers, convID, msg, maxMsgs); cerr != nil {
  1375  		err = s.handleInboxError(ctx, cerr, uid)
  1376  		return nil, err
  1377  	}
  1378  	if conv, err = s.getConvLocal(ctx, uid, convID); err != nil {
  1379  		s.Debug(ctx, "NewMessage: unable to load conversation: convID: %s err: %s", convID, err.Error())
  1380  		return nil, nil
  1381  	}
  1382  	return conv, nil
  1383  }
  1384  
  1385  func (s *HybridInboxSource) ReadMessage(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
  1386  	convID chat1.ConversationID, msgID chat1.MessageID) (conv *chat1.ConversationLocal, err error) {
  1387  	defer s.Trace(ctx, &err, "ReadMessage")()
  1388  	if cerr := s.createInbox().ReadMessage(ctx, uid, vers, convID, msgID); cerr != nil {
  1389  		err = s.handleInboxError(ctx, cerr, uid)
  1390  		return nil, err
  1391  	}
  1392  	if conv, err = s.getConvLocal(ctx, uid, convID); err != nil {
  1393  		s.Debug(ctx, "ReadMessage: unable to load conversation: convID: %s err: %s", convID, err.Error())
  1394  		return nil, nil
  1395  	}
  1396  	return conv, nil
  1397  
  1398  }
  1399  
  1400  func (s *HybridInboxSource) SetStatus(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
  1401  	convID chat1.ConversationID, status chat1.ConversationStatus) (conv *chat1.ConversationLocal, err error) {
  1402  	defer s.Trace(ctx, &err, "SetStatus")()
  1403  	if cerr := s.createInbox().SetStatus(ctx, uid, vers, convID, status); cerr != nil {
  1404  		err = s.handleInboxError(ctx, cerr, uid)
  1405  		return nil, err
  1406  	}
  1407  	if conv, err = s.getConvLocal(ctx, uid, convID); err != nil {
  1408  		s.Debug(ctx, "SetStatus: unable to load conversation: convID: %s err: %s", convID, err.Error())
  1409  		return nil, nil
  1410  	}
  1411  	return conv, nil
  1412  }
  1413  
  1414  func (s *HybridInboxSource) SetAppNotificationSettings(ctx context.Context, uid gregor1.UID,
  1415  	vers chat1.InboxVers, convID chat1.ConversationID, settings chat1.ConversationNotificationInfo) (conv *chat1.ConversationLocal, err error) {
  1416  	defer s.Trace(ctx, &err, "SetAppNotificationSettings")()
  1417  	ib := s.createInbox()
  1418  	if cerr := ib.SetAppNotificationSettings(ctx, uid, vers, convID, settings); cerr != nil {
  1419  		err = s.handleInboxError(ctx, cerr, uid)
  1420  		return nil, err
  1421  	}
  1422  	if conv, err = s.getConvLocal(ctx, uid, convID); err != nil {
  1423  		s.Debug(ctx, "SetAppNotificationSettings: unable to load conversation: convID: %s err: %s",
  1424  			convID, err.Error())
  1425  		return nil, nil
  1426  	}
  1427  	return conv, nil
  1428  }
  1429  
  1430  func (s *HybridInboxSource) TeamTypeChanged(ctx context.Context, uid gregor1.UID,
  1431  	vers chat1.InboxVers, convID chat1.ConversationID, teamType chat1.TeamType) (conv *chat1.ConversationLocal, err error) {
  1432  	defer s.Trace(ctx, &err, "TeamTypeChanged")()
  1433  
  1434  	// Read the remote conversation so we can get the notification settings changes
  1435  	remoteConv, err := utils.GetUnverifiedConv(ctx, s.G(), uid, convID,
  1436  		types.InboxSourceDataSourceRemoteOnly)
  1437  	if err != nil {
  1438  		s.Debug(ctx, "TeamTypeChanged: failed to read team type conv: %s", err.Error())
  1439  		return nil, err
  1440  	}
  1441  	ib := s.createInbox()
  1442  	if cerr := ib.TeamTypeChanged(ctx, uid, vers, convID, teamType, remoteConv.Conv.Notifications); cerr != nil {
  1443  		err = s.handleInboxError(ctx, cerr, uid)
  1444  		return nil, err
  1445  	}
  1446  	if conv, err = s.getConvLocal(ctx, uid, convID); err != nil {
  1447  		s.Debug(ctx, "TeamTypeChanged: unable to load conversation: convID: %s err: %s",
  1448  			convID, err.Error())
  1449  		return nil, nil
  1450  	}
  1451  	return conv, nil
  1452  }
  1453  
  1454  func (s *HybridInboxSource) UpgradeKBFSToImpteam(ctx context.Context, uid gregor1.UID,
  1455  	vers chat1.InboxVers, convID chat1.ConversationID) (conv *chat1.ConversationLocal, err error) {
  1456  	defer s.Trace(ctx, &err, "UpgradeKBFSToImpteam")()
  1457  
  1458  	ib := s.createInbox()
  1459  	if cerr := ib.UpgradeKBFSToImpteam(ctx, uid, vers, convID); cerr != nil {
  1460  		err = s.handleInboxError(ctx, cerr, uid)
  1461  		return nil, err
  1462  	}
  1463  	if conv, err = s.getConvLocal(ctx, uid, convID); err != nil {
  1464  		s.Debug(ctx, "UpgradeKBFSToImpteam: unable to load conversation: convID: %s err: %s",
  1465  			convID, err.Error())
  1466  		return nil, nil
  1467  	}
  1468  	return conv, nil
  1469  }
  1470  
  1471  func (s *HybridInboxSource) TlfFinalize(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
  1472  	convIDs []chat1.ConversationID, finalizeInfo chat1.ConversationFinalizeInfo) (convs []chat1.ConversationLocal, err error) {
  1473  	defer s.Trace(ctx, &err, "TlfFinalize")()
  1474  
  1475  	if cerr := s.createInbox().TlfFinalize(ctx, uid, vers, convIDs, finalizeInfo); cerr != nil {
  1476  		err = s.handleInboxError(ctx, cerr, uid)
  1477  		return convs, err
  1478  	}
  1479  	for _, convID := range convIDs {
  1480  		var conv *chat1.ConversationLocal
  1481  		if conv, err = s.getConvLocal(ctx, uid, convID); err != nil {
  1482  			s.Debug(ctx, "TlfFinalize: unable to get conversation: %s", convID)
  1483  		}
  1484  		if conv != nil {
  1485  			convs = append(convs, *conv)
  1486  		}
  1487  	}
  1488  
  1489  	// Notify rest of system about finalize
  1490  	s.notifyTlfFinalize(ctx, finalizeInfo.ResetUser)
  1491  
  1492  	return convs, nil
  1493  }
  1494  
  1495  func (s *HybridInboxSource) MembershipUpdate(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
  1496  	joined []chat1.ConversationMember, removed []chat1.ConversationMember, resets []chat1.ConversationMember,
  1497  	previews []chat1.ConversationID, teamMemberRoleUpdate *chat1.TeamMemberRoleUpdate) (res types.MembershipUpdateRes, err error) {
  1498  	defer s.Trace(ctx, &err, "MembershipUpdate")()
  1499  
  1500  	// Separate into joins and removed on uid, and then on other users
  1501  	var userJoined []chat1.ConversationID
  1502  	for _, j := range joined {
  1503  		if j.Uid.Eq(uid) {
  1504  			userJoined = append(userJoined, j.ConvID)
  1505  		} else {
  1506  			res.OthersJoinedConvs = append(res.OthersJoinedConvs, j)
  1507  		}
  1508  	}
  1509  	// Append any previewed channels as well. We can do this since we just fetch all these conversations from
  1510  	// the server, and that will have the proper member status set.
  1511  	userJoined = append(userJoined, previews...)
  1512  	for _, r := range removed {
  1513  		if r.Uid.Eq(uid) {
  1514  			// Blow away conversation cache for any conversations we get removed from
  1515  			s.Debug(ctx, "MembershipUpdate: clear conv cache for removed conv: %s", r.ConvID)
  1516  			err := s.G().ConvSource.Clear(ctx, r.ConvID, uid, nil)
  1517  			if err != nil {
  1518  				s.Debug(ctx, "MembershipUpdate: error clearing conv source: %+v", err)
  1519  			}
  1520  			res.UserRemovedConvs = append(res.UserRemovedConvs, r)
  1521  		} else {
  1522  			res.OthersRemovedConvs = append(res.OthersRemovedConvs, r)
  1523  		}
  1524  	}
  1525  
  1526  	// Load the user joined conversations
  1527  	var userJoinedConvs []chat1.Conversation
  1528  	if len(userJoined) > 0 {
  1529  		var ibox types.Inbox
  1530  		ibox, _, err = s.Read(ctx, uid, types.ConversationLocalizerBlocking,
  1531  			types.InboxSourceDataSourceRemoteOnly, nil,
  1532  			&chat1.GetInboxLocalQuery{
  1533  				ConvIDs: userJoined,
  1534  			})
  1535  		if err != nil {
  1536  			s.Debug(ctx, "MembershipUpdate: failed to read joined convs: %s", err.Error())
  1537  			return
  1538  		}
  1539  
  1540  		userJoinedConvs = utils.PluckConvs(ibox.ConvsUnverified)
  1541  		res.UserJoinedConvs = ibox.Convs
  1542  	}
  1543  
  1544  	for _, r := range resets {
  1545  		if r.Uid.Eq(uid) {
  1546  			res.UserResetConvs = append(res.UserResetConvs, r)
  1547  		} else {
  1548  			res.OthersResetConvs = append(res.OthersResetConvs, r)
  1549  		}
  1550  	}
  1551  
  1552  	ib := s.createInbox()
  1553  	roleUpdates, cerr := ib.MembershipUpdate(ctx, uid, vers, userJoinedConvs, res.UserRemovedConvs,
  1554  		res.OthersJoinedConvs, res.OthersRemovedConvs, res.UserResetConvs,
  1555  		res.OthersResetConvs, teamMemberRoleUpdate)
  1556  	if cerr != nil {
  1557  		err = s.handleInboxError(ctx, cerr, uid)
  1558  		return res, err
  1559  	}
  1560  	if len(roleUpdates) > 0 {
  1561  		convs, err := s.getConvsLocal(ctx, uid, roleUpdates)
  1562  		if err != nil {
  1563  			s.Debug(ctx, "MembershipUpdate: failed to read role update convs: %v", err)
  1564  			return res, err
  1565  		}
  1566  		res.RoleUpdates = convs
  1567  	}
  1568  
  1569  	return res, nil
  1570  }
  1571  
  1572  func (s *HybridInboxSource) ConversationsUpdate(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
  1573  	convUpdates []chat1.ConversationUpdate) (err error) {
  1574  	defer s.Trace(ctx, &err, "ConversationUpdate")()
  1575  
  1576  	ib := s.createInbox()
  1577  	if cerr := ib.ConversationsUpdate(ctx, uid, vers, convUpdates); cerr != nil {
  1578  		err = s.handleInboxError(ctx, cerr, uid)
  1579  		return err
  1580  	}
  1581  
  1582  	return nil
  1583  }
  1584  
  1585  func (s *HybridInboxSource) Expunge(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers, convID chat1.ConversationID,
  1586  	expunge chat1.Expunge, maxMsgs []chat1.MessageSummary) (*chat1.ConversationLocal, error) {
  1587  	return s.modConversation(ctx, "Expunge", uid, convID, func(ctx context.Context, ib *storage.Inbox) error {
  1588  		return ib.Expunge(ctx, uid, vers, convID, expunge, maxMsgs)
  1589  	})
  1590  }
  1591  
  1592  func (s *HybridInboxSource) SetConvRetention(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
  1593  	convID chat1.ConversationID, policy chat1.RetentionPolicy) (res *chat1.ConversationLocal, err error) {
  1594  	return s.modConversation(ctx, "SetConvRetention", uid, convID, func(ctx context.Context, ib *storage.Inbox) error {
  1595  		return ib.SetConvRetention(ctx, uid, vers, convID, policy)
  1596  	})
  1597  }
  1598  
  1599  func (s *HybridInboxSource) SetTeamRetention(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
  1600  	teamID keybase1.TeamID, policy chat1.RetentionPolicy) (convs []chat1.ConversationLocal, err error) {
  1601  	defer s.Trace(ctx, &err, "SetTeamRetention")()
  1602  	ib := s.createInbox()
  1603  	convIDs, cerr := ib.SetTeamRetention(ctx, uid, vers, teamID, policy)
  1604  	if cerr != nil {
  1605  		err = s.handleInboxError(ctx, cerr, uid)
  1606  		return nil, err
  1607  	}
  1608  	if convs, err = s.getConvsLocal(ctx, uid, convIDs); err != nil {
  1609  		s.Debug(ctx, "SetTeamRetention: unable to load conversations: convIDs: %v err: %s",
  1610  			convIDs, err.Error())
  1611  		return nil, nil
  1612  	}
  1613  	return convs, nil
  1614  }
  1615  
  1616  func (s *HybridInboxSource) SetConvSettings(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
  1617  	convID chat1.ConversationID, convSettings *chat1.ConversationSettings) (res *chat1.ConversationLocal, err error) {
  1618  	return s.modConversation(ctx, "SetConvSettings", uid, convID, func(ctx context.Context, ib *storage.Inbox) error {
  1619  		return ib.SetConvSettings(ctx, uid, vers, convID, convSettings)
  1620  	})
  1621  }
  1622  
  1623  func (s *HybridInboxSource) SubteamRename(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers,
  1624  	convIDs []chat1.ConversationID) (convs []chat1.ConversationLocal, err error) {
  1625  	defer s.Trace(ctx, &err, "SubteamRename")()
  1626  	ib := s.createInbox()
  1627  	if cerr := ib.SubteamRename(ctx, uid, vers, convIDs); cerr != nil {
  1628  		err = s.handleInboxError(ctx, cerr, uid)
  1629  		return nil, err
  1630  	}
  1631  	if convs, err = s.getConvsLocal(ctx, uid, convIDs); err != nil {
  1632  		s.Debug(ctx, "SubteamRename: unable to load conversations: convIDs: %v err: %s",
  1633  			convIDs, err.Error())
  1634  		return nil, nil
  1635  	}
  1636  	return convs, nil
  1637  }
  1638  
  1639  func (s *HybridInboxSource) UpdateInboxVersion(ctx context.Context, uid gregor1.UID, vers chat1.InboxVers) (err error) {
  1640  	defer s.Trace(ctx, &err, "UpdateInboxVersion")()
  1641  	return s.createInbox().UpdateInboxVersion(ctx, uid, vers)
  1642  }
  1643  
  1644  func (s *HybridInboxSource) modConversation(ctx context.Context, debugLabel string, uid gregor1.UID, convID chat1.ConversationID,
  1645  	mod func(context.Context, *storage.Inbox) error) (
  1646  	conv *chat1.ConversationLocal, err error) {
  1647  	defer s.Trace(ctx, &err, debugLabel)()
  1648  	ib := s.createInbox()
  1649  	if cerr := mod(ctx, ib); cerr != nil {
  1650  		err = s.handleInboxError(ctx, cerr, uid)
  1651  		return nil, err
  1652  	}
  1653  	if conv, err = s.getConvLocal(ctx, uid, convID); err != nil {
  1654  		s.Debug(ctx, "%v: unable to load conversation: convID: %s err: %v",
  1655  			debugLabel, convID, err)
  1656  		return nil, nil
  1657  	}
  1658  	return conv, nil
  1659  }
  1660  
  1661  func (s *HybridInboxSource) TeamBotSettingsForConv(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID) (
  1662  	teambotSettings map[keybase1.UID]keybase1.TeamBotSettings, err error) {
  1663  	defer s.Trace(ctx, &err, "TeamBotSettingsForConv")()
  1664  	rConv, err := s.createInbox().GetConversation(ctx, uid, convID)
  1665  	if err != nil {
  1666  		return nil, err
  1667  	}
  1668  
  1669  	metadata := rConv.Conv.Metadata
  1670  	public := metadata.Visibility == keybase1.TLFVisibility_PUBLIC
  1671  	tlfID := metadata.IdTriple.Tlfid
  1672  	infoSource := CreateNameInfoSource(ctx, s.G(), metadata.MembersType)
  1673  	var tlfName string
  1674  	if rConv.LocalMetadata == nil {
  1675  		info, err := infoSource.LookupName(ctx, tlfID, public, "")
  1676  		if err != nil {
  1677  			return nil, err
  1678  		}
  1679  		tlfName = info.CanonicalName
  1680  	} else {
  1681  		tlfName = rConv.LocalMetadata.Name
  1682  	}
  1683  
  1684  	teamBotSettings, err := infoSource.TeamBotSettings(ctx, tlfName, tlfID, metadata.MembersType, public)
  1685  	if err != nil {
  1686  		return nil, err
  1687  	}
  1688  	res := make(map[keybase1.UID]keybase1.TeamBotSettings)
  1689  	for uv, botSettings := range teamBotSettings {
  1690  		res[uv.Uid] = botSettings
  1691  	}
  1692  	return res, nil
  1693  }
  1694  
  1695  func (s *HybridInboxSource) RemoteSetConversationStatus(ctx context.Context, uid gregor1.UID,
  1696  	convID chat1.ConversationID, status chat1.ConversationStatus) (err error) {
  1697  	defer s.maybeNuke(ctx, uid, &convID, &err)
  1698  	if err := s.baseInboxSource.RemoteSetConversationStatus(ctx, uid, convID, status); err != nil {
  1699  		return err
  1700  	}
  1701  	return s.createInbox().SetStatus(ctx, uid, 0, convID, status)
  1702  }
  1703  
  1704  func (s *HybridInboxSource) RemoteDeleteConversation(ctx context.Context, uid gregor1.UID,
  1705  	convID chat1.ConversationID) (err error) {
  1706  	defer s.maybeNuke(ctx, uid, &convID, &err)
  1707  	if err := s.baseInboxSource.RemoteDeleteConversation(ctx, uid, convID); err != nil {
  1708  		return err
  1709  	}
  1710  	return s.createInbox().ConversationsUpdate(ctx, uid, 0, []chat1.ConversationUpdate{{
  1711  		ConvID:    convID,
  1712  		Existence: chat1.ConversationExistence_DELETED,
  1713  	}})
  1714  }
  1715  
  1716  func (s *HybridInboxSource) Localize(ctx context.Context, uid gregor1.UID, convs []types.RemoteConversation,
  1717  	localizerTyp types.ConversationLocalizerTyp) (res []chat1.ConversationLocal, localizeCb chan types.AsyncInboxResult, err error) {
  1718  	defer s.maybeNuke(ctx, uid, nil, &err)
  1719  	return s.baseInboxSource.Localize(ctx, uid, convs, localizerTyp)
  1720  }
  1721  
  1722  func NewInboxSource(g *globals.Context, typ string, ri func() chat1.RemoteInterface) types.InboxSource {
  1723  	switch typ {
  1724  	case "hybrid":
  1725  		return NewHybridInboxSource(g, ri)
  1726  	default:
  1727  		return NewRemoteInboxSource(g, ri)
  1728  	}
  1729  }