github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/utils/utils.go (about)

     1  package utils
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"encoding/base64"
     6  	"encoding/hex"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"math"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"mvdan.cc/xurls/v2"
    17  
    18  	"github.com/keybase/client/go/chat/pager"
    19  	"github.com/keybase/client/go/chat/unfurl/display"
    20  	"github.com/keybase/go-framed-msgpack-rpc/rpc"
    21  	"github.com/kyokomi/emoji"
    22  
    23  	"regexp"
    24  
    25  	"github.com/keybase/client/go/chat/globals"
    26  	"github.com/keybase/client/go/chat/types"
    27  	"github.com/keybase/client/go/libkb"
    28  	"github.com/keybase/client/go/logger"
    29  	"github.com/keybase/client/go/protocol/chat1"
    30  	"github.com/keybase/client/go/protocol/gregor1"
    31  	"github.com/keybase/client/go/protocol/keybase1"
    32  	"github.com/keybase/go-codec/codec"
    33  	context "golang.org/x/net/context"
    34  	"golang.org/x/net/idna"
    35  )
    36  
    37  func AssertLoggedInUID(ctx context.Context, g *globals.Context) (uid gregor1.UID, err error) {
    38  	if !g.ActiveDevice.HaveKeys() {
    39  		return uid, libkb.LoginRequiredError{}
    40  	}
    41  	k1uid := g.Env.GetUID()
    42  	if k1uid.IsNil() {
    43  		return uid, libkb.LoginRequiredError{}
    44  	}
    45  	return gregor1.UID(k1uid.ToBytes()), nil
    46  }
    47  
    48  // parseDurationExtended is like time.ParseDuration, but adds "d" unit. "1d" is
    49  // one day, defined as 24*time.Hour. Only whole days are supported for "d"
    50  // unit, but it can be followed by smaller units, e.g., "1d1h".
    51  func ParseDurationExtended(s string) (d time.Duration, err error) {
    52  	p := strings.Index(s, "d")
    53  	if p == -1 {
    54  		// no "d" suffix
    55  		return time.ParseDuration(s)
    56  	}
    57  
    58  	var days int
    59  	if days, err = strconv.Atoi(s[:p]); err != nil {
    60  		return time.Duration(0), err
    61  	}
    62  	d = time.Duration(days) * 24 * time.Hour
    63  
    64  	if p < len(s)-1 {
    65  		var dur time.Duration
    66  		if dur, err = time.ParseDuration(s[p+1:]); err != nil {
    67  			return time.Duration(0), err
    68  		}
    69  		d += dur
    70  	}
    71  
    72  	return d, nil
    73  }
    74  
    75  func ParseTimeFromRFC3339OrDurationFromPast(g *globals.Context, s string) (t time.Time, err error) {
    76  	var errt, errd error
    77  	var d time.Duration
    78  
    79  	if s == "" {
    80  		return
    81  	}
    82  
    83  	if t, errt = time.Parse(time.RFC3339, s); errt == nil {
    84  		return t, nil
    85  	}
    86  	if d, errd = ParseDurationExtended(s); errd == nil {
    87  		return g.Clock().Now().Add(-d), nil
    88  	}
    89  
    90  	return time.Time{}, fmt.Errorf("given string is neither a valid time (%s) nor a valid duration (%v)", errt, errd)
    91  }
    92  
    93  // upper bounds takes higher priority
    94  func Collar(lower int, ideal int, upper int) int {
    95  	if ideal > upper {
    96  		return upper
    97  	}
    98  	if ideal < lower {
    99  		return lower
   100  	}
   101  	return ideal
   102  }
   103  
   104  // AggRateLimitsP takes a list of rate limit responses and dedups them to the last one received
   105  // of each category
   106  func AggRateLimitsP(rlimits []*chat1.RateLimit) (res []chat1.RateLimit) {
   107  	m := make(map[string]chat1.RateLimit, len(rlimits))
   108  	for _, l := range rlimits {
   109  		if l != nil {
   110  			m[l.Name] = *l
   111  		}
   112  	}
   113  	res = make([]chat1.RateLimit, 0, len(m))
   114  	for _, v := range m {
   115  		res = append(res, v)
   116  	}
   117  	return res
   118  }
   119  
   120  func AggRateLimits(rlimits []chat1.RateLimit) (res []chat1.RateLimit) {
   121  	m := make(map[string]chat1.RateLimit, len(rlimits))
   122  	for _, l := range rlimits {
   123  		m[l.Name] = l
   124  	}
   125  	res = make([]chat1.RateLimit, 0, len(m))
   126  	for _, v := range m {
   127  		res = append(res, v)
   128  	}
   129  	return res
   130  }
   131  
   132  func ReorderParticipantsKBFS(mctx libkb.MetaContext, g libkb.UIDMapperContext, umapper libkb.UIDMapper,
   133  	tlfName string, activeList []gregor1.UID) (writerNames []chat1.ConversationLocalParticipant, err error) {
   134  	srcWriterNames, _, _, err := splitAndNormalizeTLFNameCanonicalize(mctx, tlfName, false)
   135  	if err != nil {
   136  		return writerNames, err
   137  	}
   138  	return ReorderParticipants(mctx, g, umapper, tlfName, srcWriterNames, activeList)
   139  }
   140  
   141  // ReorderParticipants based on the order in activeList.
   142  // Only allows usernames from tlfname in the output.
   143  // This never fails, worse comes to worst it just returns the split of tlfname.
   144  func ReorderParticipants(mctx libkb.MetaContext, g libkb.UIDMapperContext, umapper libkb.UIDMapper,
   145  	tlfName string, verifiedMembers []string, activeList []gregor1.UID) (writerNames []chat1.ConversationLocalParticipant, err error) {
   146  	srcWriterNames, _, _, err := splitAndNormalizeTLFNameCanonicalize(mctx, tlfName, false)
   147  	if err != nil {
   148  		return writerNames, err
   149  	}
   150  	activeKuids := make([]keybase1.UID, 0, len(activeList))
   151  	for _, a := range activeList {
   152  		activeKuids = append(activeKuids, keybase1.UID(a.String()))
   153  	}
   154  	allowedWriters := make(map[string]bool, len(verifiedMembers)+len(srcWriterNames))
   155  	for _, user := range verifiedMembers {
   156  		allowedWriters[user] = true
   157  	}
   158  	convNameUsers := make(map[string]bool, len(srcWriterNames))
   159  	for _, user := range srcWriterNames {
   160  		convNameUsers[user] = true
   161  		allowedWriters[user] = true
   162  	}
   163  
   164  	packages, err := umapper.MapUIDsToUsernamePackages(mctx.Ctx(), g, activeKuids, time.Hour*24,
   165  		10*time.Second, true)
   166  	activeMap := make(map[string]chat1.ConversationLocalParticipant)
   167  	if err == nil {
   168  		for i := 0; i < len(activeKuids); i++ {
   169  			part := UsernamePackageToParticipant(packages[i])
   170  			part.InConvName = convNameUsers[part.Username]
   171  			activeMap[activeKuids[i].String()] = part
   172  		}
   173  	}
   174  
   175  	// Fill from the active list first.
   176  	for _, uid := range activeList {
   177  		kbUID := keybase1.UID(uid.String())
   178  		p, ok := activeMap[kbUID.String()]
   179  		if !ok {
   180  			continue
   181  		}
   182  		if allowed := allowedWriters[p.Username]; allowed {
   183  			writerNames = append(writerNames, p)
   184  			// Allow only one occurrence.
   185  			allowedWriters[p.Username] = false
   186  		}
   187  	}
   188  
   189  	// Include participants even if they weren't in the active list, in stable order.
   190  	var leftOvers []chat1.ConversationLocalParticipant
   191  	for user, available := range allowedWriters {
   192  		if !available {
   193  			continue
   194  		}
   195  		part := UsernamePackageToParticipant(libkb.UsernamePackage{
   196  			NormalizedUsername: libkb.NewNormalizedUsername(user),
   197  			FullName:           nil,
   198  		})
   199  		part.InConvName = convNameUsers[part.Username]
   200  		leftOvers = append(leftOvers, part)
   201  		allowedWriters[user] = false
   202  	}
   203  	sort.Slice(leftOvers, func(i, j int) bool {
   204  		return strings.Compare(leftOvers[i].Username, leftOvers[j].Username) < 0
   205  	})
   206  	writerNames = append(writerNames, leftOvers...)
   207  
   208  	return writerNames, nil
   209  }
   210  
   211  // Drive splitAndNormalizeTLFName with one attempt to follow TlfNameNotCanonical.
   212  func splitAndNormalizeTLFNameCanonicalize(mctx libkb.MetaContext, name string, public bool) (writerNames, readerNames []string, extensionSuffix string, err error) {
   213  	writerNames, readerNames, extensionSuffix, err = SplitAndNormalizeTLFName(mctx, name, public)
   214  	if retryErr, retry := err.(TlfNameNotCanonical); retry {
   215  		return SplitAndNormalizeTLFName(mctx, retryErr.NameToTry, public)
   216  	}
   217  	return writerNames, readerNames, extensionSuffix, err
   218  }
   219  
   220  // AttachContactNames retrieves display names for SBS phones/emails that are in
   221  // the phonebook. ConversationLocalParticipant structures are modified in place
   222  // in `participants` passed in argument.
   223  func AttachContactNames(mctx libkb.MetaContext, participants []chat1.ConversationLocalParticipant) {
   224  	syncedContacts := mctx.G().SyncedContactList
   225  	if syncedContacts == nil {
   226  		mctx.Debug("AttachContactNames: SyncedContactList is nil")
   227  		return
   228  	}
   229  	var assertionToContactName map[string]string
   230  	var err error
   231  	contactsFetched := false
   232  	for i, participant := range participants {
   233  		if isPhoneOrEmail(participant.Username) {
   234  			if !contactsFetched {
   235  				assertionToContactName, err = syncedContacts.RetrieveAssertionToName(mctx)
   236  				if err != nil {
   237  					mctx.Debug("AttachContactNames: error fetching contacts: %s", err)
   238  					return
   239  				}
   240  				contactsFetched = true
   241  			}
   242  			if contactName, ok := assertionToContactName[participant.Username]; ok {
   243  				participant.ContactName = &contactName
   244  			} else {
   245  				participant.ContactName = nil
   246  			}
   247  			participants[i] = participant
   248  		}
   249  	}
   250  }
   251  
   252  func isPhoneOrEmail(username string) bool {
   253  	return strings.HasSuffix(username, "@phone") || strings.HasSuffix(username, "@email")
   254  }
   255  
   256  const (
   257  	ChatTopicIDLen    = 16
   258  	ChatTopicIDSuffix = 0x20
   259  )
   260  
   261  func NewChatTopicID() (id []byte, err error) {
   262  	if id, err = libkb.RandBytes(ChatTopicIDLen); err != nil {
   263  		return nil, err
   264  	}
   265  	id[len(id)-1] = ChatTopicIDSuffix
   266  	return id, nil
   267  }
   268  
   269  func AllChatConversationStatuses() (res []chat1.ConversationStatus) {
   270  	res = make([]chat1.ConversationStatus, 0, len(chat1.ConversationStatusMap))
   271  	for _, s := range chat1.ConversationStatusMap {
   272  		res = append(res, s)
   273  	}
   274  	sort.Sort(byConversationStatus(res))
   275  	return
   276  }
   277  
   278  // ConversationStatusBehavior describes how a ConversationStatus behaves
   279  type ConversationStatusBehavior struct {
   280  	// Whether to show the conv in the inbox
   281  	ShowInInbox bool
   282  	// Whether sending to this conv sets it back to UNFILED
   283  	SendingRemovesStatus bool
   284  	// Whether any incoming activity sets it back to UNFILED
   285  	ActivityRemovesStatus bool
   286  	// Whether to show desktop notifications
   287  	DesktopNotifications bool
   288  	// Whether to send push notifications
   289  	PushNotifications bool
   290  	// Whether to show as part of badging
   291  	ShowBadges bool
   292  }
   293  
   294  // ConversationMemberStatusBehavior describes how a ConversationMemberStatus behaves
   295  type ConversationMemberStatusBehavior struct {
   296  	// Whether to show the conv in the inbox
   297  	ShowInInbox bool
   298  	// Whether to show desktop notifications
   299  	DesktopNotifications bool
   300  	// Whether to send push notifications
   301  	PushNotifications bool
   302  	// Whether to show as part of badging
   303  	ShowBadges bool
   304  }
   305  
   306  func GetConversationMemberStatusBehavior(s chat1.ConversationMemberStatus) ConversationMemberStatusBehavior {
   307  	switch s {
   308  	case chat1.ConversationMemberStatus_ACTIVE:
   309  		return ConversationMemberStatusBehavior{
   310  			ShowInInbox:          true,
   311  			DesktopNotifications: true,
   312  			PushNotifications:    true,
   313  			ShowBadges:           true,
   314  		}
   315  	case chat1.ConversationMemberStatus_PREVIEW:
   316  		return ConversationMemberStatusBehavior{
   317  			ShowInInbox:          true,
   318  			DesktopNotifications: true,
   319  			PushNotifications:    true,
   320  			ShowBadges:           true,
   321  		}
   322  	case chat1.ConversationMemberStatus_LEFT:
   323  		return ConversationMemberStatusBehavior{
   324  			ShowInInbox:          false,
   325  			DesktopNotifications: false,
   326  			PushNotifications:    false,
   327  			ShowBadges:           false,
   328  		}
   329  	case chat1.ConversationMemberStatus_REMOVED:
   330  		return ConversationMemberStatusBehavior{
   331  			ShowInInbox:          false,
   332  			DesktopNotifications: false,
   333  			PushNotifications:    false,
   334  			ShowBadges:           false,
   335  		}
   336  	case chat1.ConversationMemberStatus_RESET:
   337  		return ConversationMemberStatusBehavior{
   338  			ShowInInbox:          true,
   339  			DesktopNotifications: false,
   340  			PushNotifications:    false,
   341  			ShowBadges:           false,
   342  		}
   343  	default:
   344  		return ConversationMemberStatusBehavior{
   345  			ShowInInbox:          true,
   346  			DesktopNotifications: true,
   347  			PushNotifications:    true,
   348  			ShowBadges:           true,
   349  		}
   350  	}
   351  }
   352  
   353  // GetConversationStatusBehavior gives information about what is allowed for a conversation status.
   354  // When changing these, be sure to update gregor's postMessage as well
   355  func GetConversationStatusBehavior(s chat1.ConversationStatus) ConversationStatusBehavior {
   356  	switch s {
   357  	case chat1.ConversationStatus_UNFILED:
   358  		return ConversationStatusBehavior{
   359  			ShowInInbox:           true,
   360  			SendingRemovesStatus:  false,
   361  			ActivityRemovesStatus: false,
   362  			DesktopNotifications:  true,
   363  			PushNotifications:     true,
   364  			ShowBadges:            true,
   365  		}
   366  	case chat1.ConversationStatus_FAVORITE:
   367  		return ConversationStatusBehavior{
   368  			ShowInInbox:           true,
   369  			SendingRemovesStatus:  false,
   370  			ActivityRemovesStatus: false,
   371  			DesktopNotifications:  true,
   372  			PushNotifications:     true,
   373  			ShowBadges:            true,
   374  		}
   375  	case chat1.ConversationStatus_IGNORED:
   376  		return ConversationStatusBehavior{
   377  			ShowInInbox:           false,
   378  			SendingRemovesStatus:  true,
   379  			ActivityRemovesStatus: true,
   380  			DesktopNotifications:  true,
   381  			PushNotifications:     true,
   382  			ShowBadges:            false,
   383  		}
   384  	case chat1.ConversationStatus_REPORTED:
   385  		fallthrough
   386  	case chat1.ConversationStatus_BLOCKED:
   387  		return ConversationStatusBehavior{
   388  			ShowInInbox:           false,
   389  			SendingRemovesStatus:  true,
   390  			ActivityRemovesStatus: false,
   391  			DesktopNotifications:  false,
   392  			PushNotifications:     false,
   393  			ShowBadges:            false,
   394  		}
   395  	case chat1.ConversationStatus_MUTED:
   396  		return ConversationStatusBehavior{
   397  			ShowInInbox:           true,
   398  			SendingRemovesStatus:  false,
   399  			ActivityRemovesStatus: false,
   400  			DesktopNotifications:  false,
   401  			PushNotifications:     false,
   402  			ShowBadges:            false,
   403  		}
   404  	default:
   405  		return ConversationStatusBehavior{
   406  			ShowInInbox:           true,
   407  			SendingRemovesStatus:  false,
   408  			ActivityRemovesStatus: false,
   409  			DesktopNotifications:  true,
   410  			PushNotifications:     true,
   411  			ShowBadges:            true,
   412  		}
   413  	}
   414  }
   415  
   416  type byConversationStatus []chat1.ConversationStatus
   417  
   418  func (c byConversationStatus) Len() int           { return len(c) }
   419  func (c byConversationStatus) Less(i, j int) bool { return c[i] < c[j] }
   420  func (c byConversationStatus) Swap(i, j int)      { c[i], c[j] = c[j], c[i] }
   421  
   422  // Which convs show in the inbox.
   423  func VisibleChatConversationStatuses() (res []chat1.ConversationStatus) {
   424  	res = make([]chat1.ConversationStatus, 0, len(chat1.ConversationStatusMap))
   425  	for _, s := range chat1.ConversationStatusMap {
   426  		if GetConversationStatusBehavior(s).ShowInInbox {
   427  			res = append(res, s)
   428  		}
   429  	}
   430  	sort.Sort(byConversationStatus(res))
   431  	return
   432  }
   433  
   434  func checkMessageTypeQual(messageType chat1.MessageType, l []chat1.MessageType) bool {
   435  	for _, mt := range l {
   436  		if messageType == mt {
   437  			return true
   438  		}
   439  	}
   440  	return false
   441  }
   442  
   443  func IsVisibleChatMessageType(messageType chat1.MessageType) bool {
   444  	return checkMessageTypeQual(messageType, chat1.VisibleChatMessageTypes())
   445  }
   446  
   447  func IsSnippetChatMessageType(messageType chat1.MessageType) bool {
   448  	return checkMessageTypeQual(messageType, chat1.SnippetChatMessageTypes())
   449  }
   450  
   451  func IsBadgeableMessageType(messageType chat1.MessageType) bool {
   452  	return checkMessageTypeQual(messageType, chat1.BadgeableMessageTypes())
   453  }
   454  
   455  func IsNonEmptyConvMessageType(messageType chat1.MessageType) bool {
   456  	return checkMessageTypeQual(messageType, chat1.NonEmptyConvMessageTypes())
   457  }
   458  
   459  func IsEditableByEditMessageType(messageType chat1.MessageType) bool {
   460  	return checkMessageTypeQual(messageType, chat1.EditableMessageTypesByEdit())
   461  }
   462  
   463  func IsDeleteableByDeleteMessageType(valid chat1.MessageUnboxedValid) bool {
   464  	if !checkMessageTypeQual(valid.ClientHeader.MessageType, chat1.DeletableMessageTypesByDelete()) {
   465  		return false
   466  	}
   467  	if !valid.MessageBody.IsType(chat1.MessageType_SYSTEM) {
   468  		return true
   469  	}
   470  	sysMsg := valid.MessageBody.System()
   471  	typ, err := sysMsg.SystemType()
   472  	if err != nil {
   473  		return true
   474  	}
   475  	return chat1.IsSystemMsgDeletableByDelete(typ)
   476  }
   477  
   478  func IsCollapsibleMessageType(messageType chat1.MessageType) bool {
   479  	switch messageType {
   480  	case chat1.MessageType_UNFURL, chat1.MessageType_ATTACHMENT:
   481  		return true
   482  	}
   483  	return false
   484  }
   485  
   486  func IsNotifiableChatMessageType(messageType chat1.MessageType, atMentions []gregor1.UID,
   487  	chanMention chat1.ChannelMention) bool {
   488  	switch messageType {
   489  	case chat1.MessageType_EDIT:
   490  		// an edit with atMention or channel mention should generate notifications
   491  		return len(atMentions) > 0 || chanMention != chat1.ChannelMention_NONE
   492  	case chat1.MessageType_REACTION:
   493  		// effect of this is all reactions will notify if they are sent to a person that
   494  		// is notified for any messages in the conversation
   495  		return true
   496  	case chat1.MessageType_JOIN, chat1.MessageType_LEAVE:
   497  		return false
   498  	default:
   499  		return IsVisibleChatMessageType(messageType)
   500  	}
   501  }
   502  
   503  type DebugLabeler struct {
   504  	libkb.Contextified
   505  	label   string
   506  	verbose bool
   507  }
   508  
   509  func NewDebugLabeler(g *libkb.GlobalContext, label string, verbose bool) DebugLabeler {
   510  	return DebugLabeler{
   511  		Contextified: libkb.NewContextified(g),
   512  		label:        label,
   513  		verbose:      verbose,
   514  	}
   515  }
   516  
   517  func (d DebugLabeler) GetLog() logger.Logger {
   518  	return d.G().GetLog()
   519  }
   520  
   521  func (d DebugLabeler) GetPerfLog() logger.Logger {
   522  	return d.G().GetPerfLog()
   523  }
   524  
   525  func (d DebugLabeler) showVerbose() bool {
   526  	return false
   527  }
   528  
   529  func (d DebugLabeler) showLog() bool {
   530  	if d.verbose {
   531  		return d.showVerbose()
   532  	}
   533  	return true
   534  }
   535  
   536  func (d DebugLabeler) Debug(ctx context.Context, msg string, args ...interface{}) {
   537  	if d.showLog() {
   538  		d.G().GetLog().CDebugf(ctx, "++Chat: | "+d.label+": "+msg, args...)
   539  	}
   540  }
   541  
   542  func (d DebugLabeler) Trace(ctx context.Context, err *error, format string, args ...interface{}) func() {
   543  	return d.trace(ctx, d.G().GetLog(), err, format, args...)
   544  }
   545  
   546  func (d DebugLabeler) PerfTrace(ctx context.Context, err *error, format string, args ...interface{}) func() {
   547  	return d.trace(ctx, d.G().GetPerfLog(), err, format, args...)
   548  }
   549  
   550  func (d DebugLabeler) trace(ctx context.Context, log logger.Logger, err *error, format string, args ...interface{}) func() {
   551  	if d.showLog() {
   552  		msg := fmt.Sprintf(format, args...)
   553  		start := time.Now()
   554  		log.CDebugf(ctx, "++Chat: + %s: %s", d.label, msg)
   555  		return func() {
   556  			log.CDebugf(ctx, "++Chat: - %s: %s -> %s [time=%v]", d.label, msg,
   557  				libkb.ErrToOkPtr(err), time.Since(start))
   558  		}
   559  	}
   560  	return func() {}
   561  }
   562  
   563  // FilterByType filters messages based on a query.
   564  // If includeAllErrors then MessageUnboxedError are all returned. Otherwise, they are filtered based on type.
   565  // Messages whose type cannot be determined are considered errors.
   566  func FilterByType(msgs []chat1.MessageUnboxed, query *chat1.GetThreadQuery, includeAllErrors bool) (res []chat1.MessageUnboxed) {
   567  	useTypeFilter := (query != nil && len(query.MessageTypes) > 0)
   568  
   569  	typmap := make(map[chat1.MessageType]bool)
   570  	if useTypeFilter {
   571  		for _, mt := range query.MessageTypes {
   572  			typmap[mt] = true
   573  		}
   574  	}
   575  
   576  	for _, msg := range msgs {
   577  		state, err := msg.State()
   578  		if err != nil {
   579  			if includeAllErrors {
   580  				res = append(res, msg)
   581  			}
   582  			continue
   583  		}
   584  		switch state {
   585  		case chat1.MessageUnboxedState_ERROR:
   586  			if includeAllErrors {
   587  				res = append(res, msg)
   588  			}
   589  		case chat1.MessageUnboxedState_PLACEHOLDER:
   590  			// We don't know what the type is for these, so just include them
   591  			res = append(res, msg)
   592  		default:
   593  			_, match := typmap[msg.GetMessageType()]
   594  			if !useTypeFilter || match {
   595  				res = append(res, msg)
   596  			}
   597  		}
   598  	}
   599  	return res
   600  }
   601  
   602  // Filter messages that are both exploded that are no longer shown in the GUI
   603  // (as ash lines)
   604  func FilterExploded(conv types.UnboxConversationInfo, msgs []chat1.MessageUnboxed, now time.Time) (res []chat1.MessageUnboxed) {
   605  	upto := conv.GetMaxDeletedUpTo()
   606  	for _, msg := range msgs {
   607  		if msg.IsEphemeral() && msg.HideExplosion(upto, now) {
   608  			continue
   609  		}
   610  		res = append(res, msg)
   611  	}
   612  	return res
   613  }
   614  
   615  func GetReaction(msg chat1.MessageUnboxed) (string, error) {
   616  	if !msg.IsValid() {
   617  		return "", errors.New("invalid message")
   618  	}
   619  	body := msg.Valid().MessageBody
   620  	typ, err := body.MessageType()
   621  	if err != nil {
   622  		return "", err
   623  	}
   624  	if typ != chat1.MessageType_REACTION {
   625  		return "", fmt.Errorf("not a reaction type: %v", typ)
   626  	}
   627  	return body.Reaction().Body, nil
   628  }
   629  
   630  // GetSupersedes must be called with a valid msg
   631  func GetSupersedes(msg chat1.MessageUnboxed) ([]chat1.MessageID, error) {
   632  	if !msg.IsValidFull() {
   633  		return nil, fmt.Errorf("GetSupersedes called with invalid message: %v", msg.GetMessageID())
   634  	}
   635  	body := msg.Valid().MessageBody
   636  	typ, err := body.MessageType()
   637  	if err != nil {
   638  		return nil, err
   639  	}
   640  
   641  	// We use the message ID in the body over the field in the client header to
   642  	// avoid server trust.
   643  	switch typ {
   644  	case chat1.MessageType_EDIT:
   645  		return []chat1.MessageID{msg.Valid().MessageBody.Edit().MessageID}, nil
   646  	case chat1.MessageType_REACTION:
   647  		return []chat1.MessageID{msg.Valid().MessageBody.Reaction().MessageID}, nil
   648  	case chat1.MessageType_DELETE:
   649  		return msg.Valid().MessageBody.Delete().MessageIDs, nil
   650  	case chat1.MessageType_ATTACHMENTUPLOADED:
   651  		return []chat1.MessageID{msg.Valid().MessageBody.Attachmentuploaded().MessageID}, nil
   652  	case chat1.MessageType_UNFURL:
   653  		return []chat1.MessageID{msg.Valid().MessageBody.Unfurl().MessageID}, nil
   654  	default:
   655  		return nil, nil
   656  	}
   657  }
   658  
   659  // Start at the beginning of the line, space, or some hand picked artisanal
   660  // characters
   661  const ServiceDecorationPrefix = `(?:^|[\s([/{:;.,!?"'])`
   662  
   663  var chanNameMentionRegExp = regexp.MustCompile(ServiceDecorationPrefix + `(#(?:[0-9a-zA-Z_-]+))`)
   664  
   665  func ParseChannelNameMentions(ctx context.Context, body string, uid gregor1.UID, teamID chat1.TLFID,
   666  	ts types.TeamChannelSource) (res []chat1.ChannelNameMention) {
   667  	names := parseRegexpNames(ctx, body, chanNameMentionRegExp)
   668  	if len(names) == 0 {
   669  		return nil
   670  	}
   671  	chanResponse, err := ts.GetChannelsTopicName(ctx, uid, teamID, chat1.TopicType_CHAT)
   672  	if err != nil {
   673  		return nil
   674  	}
   675  	validChans := make(map[string]chat1.ChannelNameMention)
   676  	for _, cr := range chanResponse {
   677  		validChans[cr.TopicName] = cr
   678  	}
   679  	for _, name := range names {
   680  		if cr, ok := validChans[name.name]; ok {
   681  			res = append(res, cr)
   682  		}
   683  	}
   684  	return res
   685  }
   686  
   687  var atMentionRegExp = regexp.MustCompile(ServiceDecorationPrefix +
   688  	`(@(?:[a-zA-Z0-9][a-zA-Z0-9._]*[a-zA-Z0-9_]+(?:#[a-z0-9A-Z_-]+)?))`)
   689  
   690  type nameMatch struct {
   691  	name, normalizedName string
   692  	position             []int
   693  }
   694  
   695  func (m nameMatch) Len() int {
   696  	return m.position[1] - m.position[0]
   697  }
   698  
   699  func parseRegexpNames(ctx context.Context, body string, re *regexp.Regexp) (res []nameMatch) {
   700  	body = ReplaceQuotedSubstrings(body, true)
   701  	allIndexMatches := re.FindAllStringSubmatchIndex(body, -1)
   702  	for _, indexMatch := range allIndexMatches {
   703  		if len(indexMatch) >= 4 {
   704  			// do +1 so we don't include the @ in the hit.
   705  			low := indexMatch[2] + 1
   706  			high := indexMatch[3]
   707  			hit := body[low:high]
   708  			res = append(res, nameMatch{
   709  				name:           hit,
   710  				normalizedName: strings.ToLower(hit),
   711  				position:       []int{low, high},
   712  			})
   713  		}
   714  	}
   715  	return res
   716  }
   717  
   718  func GetTextAtMentionedItems(ctx context.Context, g *globals.Context, uid gregor1.UID,
   719  	convID chat1.ConversationID, msg chat1.MessageText,
   720  	getConvMembs func() ([]string, error),
   721  	debug *DebugLabeler) (atRes []chat1.KnownUserMention, maybeRes []chat1.MaybeMention, chanRes chat1.ChannelMention) {
   722  	atRes, maybeRes, chanRes = ParseAtMentionedItems(ctx, g, msg.Body, msg.UserMentions, getConvMembs)
   723  	atRes = append(atRes, GetPaymentAtMentions(ctx, g.GetUPAKLoader(), msg.Payments, debug)...)
   724  	if msg.ReplyToUID != nil {
   725  		atRes = append(atRes, chat1.KnownUserMention{
   726  			Text: "",
   727  			Uid:  *msg.ReplyToUID,
   728  		})
   729  	}
   730  	return atRes, maybeRes, chanRes
   731  }
   732  
   733  func GetPaymentAtMentions(ctx context.Context, upak libkb.UPAKLoader, payments []chat1.TextPayment,
   734  	l *DebugLabeler) (atMentions []chat1.KnownUserMention) {
   735  	for _, p := range payments {
   736  		uid, err := upak.LookupUID(ctx, libkb.NewNormalizedUsername(p.Username))
   737  		if err != nil {
   738  			l.Debug(ctx, "GetPaymentAtMentions: error loading uid: username: %s err: %s", p.Username, err)
   739  			continue
   740  		}
   741  		atMentions = append(atMentions, chat1.KnownUserMention{
   742  			Uid:  uid.ToBytes(),
   743  			Text: "",
   744  		})
   745  	}
   746  	return atMentions
   747  }
   748  
   749  func parseItemAsUID(ctx context.Context, g *globals.Context, name string,
   750  	knownMentions []chat1.KnownUserMention,
   751  	getConvMembs func() ([]string, error)) (gregor1.UID, error) {
   752  	nname := libkb.NewNormalizedUsername(name)
   753  	shouldLookup := false
   754  	for _, known := range knownMentions {
   755  		if known.Text == nname.String() {
   756  			shouldLookup = true
   757  			break
   758  		}
   759  	}
   760  	if !shouldLookup {
   761  		shouldLookup = libkb.IsUserByUsernameOffline(libkb.NewMetaContext(ctx, g.ExternalG()), nname)
   762  	}
   763  	if !shouldLookup && getConvMembs != nil {
   764  		membs, err := getConvMembs()
   765  		if err != nil {
   766  			return nil, err
   767  		}
   768  		for _, memb := range membs {
   769  			if memb == nname.String() {
   770  				shouldLookup = true
   771  				break
   772  			}
   773  		}
   774  	}
   775  	if shouldLookup {
   776  		kuid, err := g.GetUPAKLoader().LookupUID(ctx, nname)
   777  		if err != nil {
   778  			return nil, err
   779  		}
   780  		return kuid.ToBytes(), nil
   781  	}
   782  	return nil, errors.New("not a username")
   783  }
   784  
   785  func ParseAtMentionedItems(ctx context.Context, g *globals.Context, body string,
   786  	knownMentions []chat1.KnownUserMention, getConvMembs func() ([]string, error)) (atRes []chat1.KnownUserMention, maybeRes []chat1.MaybeMention, chanRes chat1.ChannelMention) {
   787  	matches := parseRegexpNames(ctx, body, atMentionRegExp)
   788  	chanRes = chat1.ChannelMention_NONE
   789  	for _, m := range matches {
   790  		var channel string
   791  		toks := strings.Split(m.name, "#")
   792  		baseName := toks[0]
   793  		if len(toks) > 1 {
   794  			channel = toks[1]
   795  		}
   796  
   797  		normalizedBaseName := strings.Split(m.normalizedName, "#")[0]
   798  		switch normalizedBaseName {
   799  		case "channel", "everyone":
   800  			chanRes = chat1.ChannelMention_ALL
   801  			continue
   802  		case "here":
   803  			if chanRes != chat1.ChannelMention_ALL {
   804  				chanRes = chat1.ChannelMention_HERE
   805  			}
   806  			continue
   807  		default:
   808  		}
   809  
   810  		// Try UID first then team
   811  		if uid, err := parseItemAsUID(ctx, g, normalizedBaseName, knownMentions, getConvMembs); err == nil {
   812  			atRes = append(atRes, chat1.KnownUserMention{
   813  				Text: baseName,
   814  				Uid:  uid,
   815  			})
   816  		} else {
   817  			// anything else is a possible mention
   818  			maybeRes = append(maybeRes, chat1.MaybeMention{
   819  				Name:    baseName,
   820  				Channel: channel,
   821  			})
   822  		}
   823  	}
   824  	return atRes, maybeRes, chanRes
   825  }
   826  
   827  func SystemMessageMentions(ctx context.Context, g *globals.Context, uid gregor1.UID,
   828  	body chat1.MessageSystem) (atMentions []gregor1.UID, chanMention chat1.ChannelMention, channelNameMentions []chat1.ChannelNameMention) {
   829  	typ, err := body.SystemType()
   830  	if err != nil {
   831  		return nil, 0, nil
   832  	}
   833  	switch typ {
   834  	case chat1.MessageSystemType_ADDEDTOTEAM:
   835  		addeeUID, err := g.GetUPAKLoader().LookupUID(ctx,
   836  			libkb.NewNormalizedUsername(body.Addedtoteam().Addee))
   837  		if err == nil {
   838  			atMentions = append(atMentions, addeeUID.ToBytes())
   839  		}
   840  	case chat1.MessageSystemType_INVITEADDEDTOTEAM:
   841  		inviteeUID, err := g.GetUPAKLoader().LookupUID(ctx,
   842  			libkb.NewNormalizedUsername(body.Inviteaddedtoteam().Invitee))
   843  		if err == nil {
   844  			atMentions = append(atMentions, inviteeUID.ToBytes())
   845  		}
   846  		inviterUID, err := g.GetUPAKLoader().LookupUID(ctx,
   847  			libkb.NewNormalizedUsername(body.Inviteaddedtoteam().Inviter))
   848  		if err == nil {
   849  			atMentions = append(atMentions, inviterUID.ToBytes())
   850  		}
   851  	case chat1.MessageSystemType_COMPLEXTEAM:
   852  		chanMention = chat1.ChannelMention_ALL
   853  	case chat1.MessageSystemType_BULKADDTOCONV:
   854  		for _, username := range body.Bulkaddtoconv().Usernames {
   855  			uid, err := g.GetUPAKLoader().LookupUID(ctx, libkb.NewNormalizedUsername(username))
   856  			if err == nil {
   857  				atMentions = append(atMentions, uid.ToBytes())
   858  			}
   859  		}
   860  	case chat1.MessageSystemType_NEWCHANNEL:
   861  		conv, err := GetVerifiedConv(ctx, g, uid, body.Newchannel().ConvID, types.InboxSourceDataSourceAll)
   862  		if err == nil {
   863  			channelNameMentions = append(channelNameMentions, chat1.ChannelNameMention{
   864  				ConvID:    conv.GetConvID(),
   865  				TopicName: conv.GetTopicName(),
   866  			})
   867  		}
   868  	}
   869  	sort.Sort(chat1.ByUID(atMentions))
   870  	return atMentions, chanMention, channelNameMentions
   871  }
   872  
   873  func PluckMessageIDs(msgs []chat1.MessageSummary) []chat1.MessageID {
   874  	res := make([]chat1.MessageID, len(msgs))
   875  	for i, m := range msgs {
   876  		res[i] = m.GetMessageID()
   877  	}
   878  	return res
   879  }
   880  
   881  func PluckUIMessageIDs(msgs []chat1.UIMessage) (res []chat1.MessageID) {
   882  	res = make([]chat1.MessageID, 0, len(msgs))
   883  	for _, m := range msgs {
   884  		res = append(res, m.GetMessageID())
   885  	}
   886  	return res
   887  }
   888  
   889  func PluckMUMessageIDs(msgs []chat1.MessageUnboxed) (res []chat1.MessageID) {
   890  	res = make([]chat1.MessageID, 0, len(msgs))
   891  	for _, m := range msgs {
   892  		res = append(res, m.GetMessageID())
   893  	}
   894  	return res
   895  }
   896  
   897  func IsConvEmpty(conv chat1.Conversation) bool {
   898  	switch conv.GetMembersType() {
   899  	case chat1.ConversationMembersType_TEAM:
   900  		return false
   901  	default:
   902  		for _, msg := range conv.MaxMsgSummaries {
   903  			if IsNonEmptyConvMessageType(msg.GetMessageType()) {
   904  				return false
   905  			}
   906  		}
   907  		return true
   908  	}
   909  }
   910  
   911  func PluckConvIDsLocal(convs []chat1.ConversationLocal) (res []chat1.ConversationID) {
   912  	res = make([]chat1.ConversationID, 0, len(convs))
   913  	for _, conv := range convs {
   914  		res = append(res, conv.GetConvID())
   915  	}
   916  	return res
   917  }
   918  
   919  func PluckConvIDs(convs []chat1.Conversation) (res []chat1.ConversationID) {
   920  	res = make([]chat1.ConversationID, 0, len(convs))
   921  	for _, conv := range convs {
   922  		res = append(res, conv.GetConvID())
   923  	}
   924  	return res
   925  }
   926  
   927  func PluckConvIDsRC(convs []types.RemoteConversation) (res []chat1.ConversationID) {
   928  	res = make([]chat1.ConversationID, 0, len(convs))
   929  	for _, conv := range convs {
   930  		res = append(res, conv.GetConvID())
   931  	}
   932  	return res
   933  }
   934  
   935  func SanitizeTopicName(topicName string) string {
   936  	return strings.TrimPrefix(topicName, "#")
   937  }
   938  
   939  func CreateTopicNameState(cmp chat1.ConversationIDMessageIDPairs) (chat1.TopicNameState, error) {
   940  	var data []byte
   941  	var err error
   942  	mh := codec.MsgpackHandle{WriteExt: true}
   943  	enc := codec.NewEncoderBytes(&data, &mh)
   944  	if err = enc.Encode(cmp); err != nil {
   945  		return chat1.TopicNameState{}, err
   946  	}
   947  
   948  	h := sha256.New()
   949  	if _, err = h.Write(data); err != nil {
   950  		return chat1.TopicNameState{}, err
   951  	}
   952  
   953  	return h.Sum(nil), nil
   954  }
   955  
   956  func GetConvLastSendTime(rc types.RemoteConversation) gregor1.Time {
   957  	conv := rc.Conv
   958  	if conv.ReaderInfo == nil {
   959  		return 0
   960  	}
   961  	if conv.ReaderInfo.LastSendTime == 0 {
   962  		return GetConvMtime(rc)
   963  	}
   964  	return conv.ReaderInfo.LastSendTime
   965  }
   966  
   967  func GetConvMtime(rc types.RemoteConversation) (res gregor1.Time) {
   968  	conv := rc.Conv
   969  	var summaries []chat1.MessageSummary
   970  	for _, typ := range chat1.VisibleChatMessageTypes() {
   971  		summary, err := conv.GetMaxMessage(typ)
   972  		if err == nil {
   973  			summaries = append(summaries, summary)
   974  		}
   975  	}
   976  	sort.Sort(ByMsgSummaryCtime(summaries))
   977  	if len(summaries) == 0 {
   978  		res = conv.ReaderInfo.Mtime
   979  	} else {
   980  		res = summaries[len(summaries)-1].Ctime
   981  	}
   982  	if res > rc.LocalMtime {
   983  		return res
   984  	}
   985  	return rc.LocalMtime
   986  }
   987  
   988  // GetConvPriorityScore weighs conversations that are fully read above ones
   989  // that are not, weighting more recently modified conversations higher.. Used
   990  // to order conversations when background loading.
   991  func GetConvPriorityScore(rc types.RemoteConversation) float64 {
   992  	readMsgID := rc.GetReadMsgID()
   993  	maxMsgID := rc.Conv.ReaderInfo.MaxMsgid
   994  	mtime := GetConvMtime(rc)
   995  	dur := math.Abs(float64(time.Since(mtime.Time())) / float64(time.Hour))
   996  	return 100 / math.Pow(dur+float64(maxMsgID-readMsgID), 0.5)
   997  }
   998  
   999  type MessageSummaryContainer interface {
  1000  	GetMaxMessage(typ chat1.MessageType) (chat1.MessageSummary, error)
  1001  }
  1002  
  1003  func PickLatestMessageSummary(conv MessageSummaryContainer, typs []chat1.MessageType) (res chat1.MessageSummary, err error) {
  1004  	// nil means all
  1005  	if typs == nil {
  1006  		for typ := range chat1.MessageTypeRevMap {
  1007  			typs = append(typs, typ)
  1008  		}
  1009  	}
  1010  	for _, typ := range typs {
  1011  		msg, err := conv.GetMaxMessage(typ)
  1012  		if err == nil && (msg.Ctime.After(res.Ctime) || res.Ctime.IsZero()) {
  1013  			res = msg
  1014  		}
  1015  	}
  1016  	if res.GetMessageID() == 0 {
  1017  		return res, errors.New("no message summary found")
  1018  	}
  1019  	return res, nil
  1020  }
  1021  
  1022  func GetConvMtimeLocal(conv chat1.ConversationLocal) gregor1.Time {
  1023  	msg, err := PickLatestMessageSummary(conv, chat1.VisibleChatMessageTypes())
  1024  	if err != nil {
  1025  		return conv.ReaderInfo.Mtime
  1026  	}
  1027  	return msg.Ctime
  1028  }
  1029  
  1030  func GetRemoteConvTLFName(conv types.RemoteConversation) string {
  1031  	if conv.LocalMetadata != nil {
  1032  		return conv.LocalMetadata.Name
  1033  	}
  1034  	msg, err := PickLatestMessageSummary(conv.Conv, nil)
  1035  	if err != nil {
  1036  		return ""
  1037  	}
  1038  	return msg.TlfName
  1039  }
  1040  
  1041  func GetRemoteConvDisplayName(rc types.RemoteConversation) string {
  1042  	tlfName := GetRemoteConvTLFName(rc)
  1043  	switch rc.Conv.Metadata.TeamType {
  1044  	case chat1.TeamType_COMPLEX:
  1045  		if rc.LocalMetadata != nil && len(rc.Conv.MaxMsgSummaries) > 0 {
  1046  			return fmt.Sprintf("%s#%s", tlfName, rc.LocalMetadata.TopicName)
  1047  		}
  1048  		fallthrough
  1049  	default:
  1050  		return tlfName
  1051  	}
  1052  }
  1053  
  1054  func GetConvSnippet(ctx context.Context, g *globals.Context, uid gregor1.UID, conv chat1.ConversationLocal,
  1055  	currentUsername string) (chat1.SnippetDecoration, string, string) {
  1056  
  1057  	if conv.Info.SnippetMsg == nil {
  1058  		return chat1.SnippetDecoration_NONE, "", ""
  1059  	}
  1060  	msg := *conv.Info.SnippetMsg
  1061  
  1062  	return GetMsgSnippet(ctx, g, uid, msg, conv, currentUsername)
  1063  }
  1064  
  1065  func GetMsgSummaryByType(msgs []chat1.MessageSummary, typ chat1.MessageType) (chat1.MessageSummary, error) {
  1066  	for _, msg := range msgs {
  1067  		if msg.GetMessageType() == typ {
  1068  			return msg, nil
  1069  		}
  1070  	}
  1071  	return chat1.MessageSummary{}, errors.New("not found")
  1072  }
  1073  
  1074  func showSenderPrefix(conv chat1.ConversationLocal) (showPrefix bool) {
  1075  	switch conv.GetMembersType() {
  1076  	case chat1.ConversationMembersType_TEAM:
  1077  		showPrefix = true
  1078  	default:
  1079  		showPrefix = len(conv.AllNames()) > 2
  1080  	}
  1081  	return showPrefix
  1082  }
  1083  
  1084  // Sender prefix for msg snippets. Will show if a conversation has > 2 members
  1085  // or is of type TEAM
  1086  func getSenderPrefix(conv chat1.ConversationLocal, currentUsername, senderUsername string) (senderPrefix string) {
  1087  	if showSenderPrefix(conv) {
  1088  		if senderUsername == currentUsername {
  1089  			senderPrefix = "You: "
  1090  		} else {
  1091  			senderPrefix = fmt.Sprintf("%s: ", senderUsername)
  1092  		}
  1093  	}
  1094  	return senderPrefix
  1095  }
  1096  
  1097  func formatDuration(dur time.Duration) string {
  1098  	h := dur / time.Hour
  1099  	dur -= h * time.Hour
  1100  	m := dur / time.Minute
  1101  	dur -= m * time.Minute
  1102  	s := dur / time.Second
  1103  	if h > 0 {
  1104  		return fmt.Sprintf("%02d:%02d:%02d", h, m, s)
  1105  	}
  1106  	return fmt.Sprintf("%02d:%02d", m, s)
  1107  }
  1108  
  1109  func getMsgSnippetDecoration(msg chat1.MessageUnboxed) chat1.SnippetDecoration {
  1110  	var msgBody chat1.MessageBody
  1111  	if msg.IsValid() {
  1112  		msgBody = msg.Valid().MessageBody
  1113  	} else {
  1114  		msgBody = msg.Outbox().Msg.MessageBody
  1115  	}
  1116  	switch msg.GetMessageType() {
  1117  	case chat1.MessageType_ATTACHMENT:
  1118  		obj := msgBody.Attachment().Object
  1119  		atyp, err := obj.Metadata.AssetType()
  1120  		if err != nil {
  1121  			return chat1.SnippetDecoration_NONE
  1122  		}
  1123  		switch atyp {
  1124  		case chat1.AssetMetadataType_IMAGE:
  1125  			return chat1.SnippetDecoration_PHOTO_ATTACHMENT
  1126  		case chat1.AssetMetadataType_VIDEO:
  1127  			if obj.Metadata.Video().IsAudio {
  1128  				return chat1.SnippetDecoration_AUDIO_ATTACHMENT
  1129  			}
  1130  			return chat1.SnippetDecoration_VIDEO_ATTACHMENT
  1131  		}
  1132  		return chat1.SnippetDecoration_FILE_ATTACHMENT
  1133  	case chat1.MessageType_REQUESTPAYMENT:
  1134  		return chat1.SnippetDecoration_STELLAR_RECEIVED
  1135  	case chat1.MessageType_SENDPAYMENT:
  1136  		return chat1.SnippetDecoration_STELLAR_SENT
  1137  	case chat1.MessageType_PIN:
  1138  		return chat1.SnippetDecoration_PINNED_MESSAGE
  1139  	}
  1140  	return chat1.SnippetDecoration_NONE
  1141  }
  1142  
  1143  func GetMsgSnippetBody(ctx context.Context, g *globals.Context, uid gregor1.UID, convID chat1.ConversationID,
  1144  	msg chat1.MessageUnboxed) (snippet, snippetDecorated string) {
  1145  	if !(msg.IsValidFull() || msg.IsOutbox()) {
  1146  		return "", ""
  1147  	}
  1148  	defer func() {
  1149  		if len(snippetDecorated) == 0 {
  1150  			snippetDecorated = EscapeShrugs(ctx, snippet)
  1151  		}
  1152  	}()
  1153  	var msgBody chat1.MessageBody
  1154  	var emojis []chat1.HarvestedEmoji
  1155  	if msg.IsValid() {
  1156  		msgBody = msg.Valid().MessageBody
  1157  		emojis = msg.Valid().Emojis
  1158  	} else {
  1159  		msgBody = msg.Outbox().Msg.MessageBody
  1160  		emojis = msg.Outbox().Msg.Emojis
  1161  	}
  1162  	switch msg.GetMessageType() {
  1163  	case chat1.MessageType_TEXT:
  1164  		return msgBody.Text().Body,
  1165  			PresentDecoratedSnippet(ctx, g, msgBody.Text().Body, uid, msg.GetMessageType(), emojis)
  1166  	case chat1.MessageType_EDIT:
  1167  		return msgBody.Edit().Body, ""
  1168  	case chat1.MessageType_FLIP:
  1169  		return msgBody.Flip().Text, ""
  1170  	case chat1.MessageType_PIN:
  1171  		return "Pinned message", ""
  1172  	case chat1.MessageType_ATTACHMENT:
  1173  		obj := msgBody.Attachment().Object
  1174  		title := obj.Title
  1175  		if len(title) == 0 {
  1176  			atyp, err := obj.Metadata.AssetType()
  1177  			if err != nil {
  1178  				return "???", ""
  1179  			}
  1180  			switch atyp {
  1181  			case chat1.AssetMetadataType_IMAGE:
  1182  				title = "Image attachment"
  1183  			case chat1.AssetMetadataType_VIDEO:
  1184  				dur := formatDuration(time.Duration(obj.Metadata.Video().DurationMs) * time.Millisecond)
  1185  				if obj.Metadata.Video().IsAudio {
  1186  					title = fmt.Sprintf("Audio message (%s)", dur)
  1187  				} else {
  1188  					title = fmt.Sprintf("Video attachment (%s)", dur)
  1189  				}
  1190  			default:
  1191  				if obj.Filename == "" {
  1192  					title = "File attachment"
  1193  				} else {
  1194  					title = obj.Filename
  1195  				}
  1196  			}
  1197  		}
  1198  		return title, ""
  1199  	case chat1.MessageType_SYSTEM:
  1200  		return msgBody.System().String(), ""
  1201  	case chat1.MessageType_REQUESTPAYMENT:
  1202  		return "Payment requested", ""
  1203  	case chat1.MessageType_SENDPAYMENT:
  1204  		return "Payment sent", ""
  1205  	case chat1.MessageType_HEADLINE:
  1206  		return msgBody.Headline().String(), ""
  1207  	}
  1208  	return "", ""
  1209  }
  1210  
  1211  func GetMsgSnippet(ctx context.Context, g *globals.Context, uid gregor1.UID, msg chat1.MessageUnboxed,
  1212  	conv chat1.ConversationLocal, currentUsername string) (decoration chat1.SnippetDecoration, snippet string, snippetDecorated string) {
  1213  	if !(msg.IsValid() || msg.IsOutbox()) {
  1214  		return chat1.SnippetDecoration_NONE, "", ""
  1215  	}
  1216  	defer func() {
  1217  		if len(snippetDecorated) == 0 {
  1218  			snippetDecorated = snippet
  1219  		}
  1220  	}()
  1221  
  1222  	var senderUsername string
  1223  	if msg.IsValid() {
  1224  		senderUsername = msg.Valid().SenderUsername
  1225  	} else {
  1226  		senderUsername = currentUsername
  1227  	}
  1228  
  1229  	senderPrefix := getSenderPrefix(conv, currentUsername, senderUsername)
  1230  	// does not apply to outbox messages, ephemeral timer starts once the server
  1231  	// assigns a ctime.
  1232  	if msg.IsValid() && !msg.IsValidFull() {
  1233  		if msg.Valid().IsEphemeral() && msg.Valid().IsEphemeralExpired(time.Now()) {
  1234  			return chat1.SnippetDecoration_EXPLODED_MESSAGE, "Message exploded.", ""
  1235  		}
  1236  		return chat1.SnippetDecoration_NONE, "", ""
  1237  	}
  1238  
  1239  	if msg.IsOutbox() && msg.Outbox().IsBadgable() {
  1240  		decoration = chat1.SnippetDecoration_PENDING_MESSAGE
  1241  		if msg.Outbox().IsError() {
  1242  			decoration = chat1.SnippetDecoration_FAILED_PENDING_MESSAGE
  1243  		}
  1244  	} else if msg.IsValid() && msg.Valid().IsEphemeral() {
  1245  		decoration = chat1.SnippetDecoration_EXPLODING_MESSAGE
  1246  	} else {
  1247  		decoration = getMsgSnippetDecoration(msg)
  1248  	}
  1249  	snippet, snippetDecorated = GetMsgSnippetBody(ctx, g, uid, conv.GetConvID(), msg)
  1250  	if snippet == "" {
  1251  		decoration = chat1.SnippetDecoration_NONE
  1252  	}
  1253  	return decoration, senderPrefix + snippet, senderPrefix + snippetDecorated
  1254  }
  1255  
  1256  func GetDesktopNotificationSnippet(ctx context.Context, g *globals.Context,
  1257  	uid gregor1.UID, conv *chat1.ConversationLocal, currentUsername string,
  1258  	fromMsg *chat1.MessageUnboxed, plaintextDesktopDisabled bool) string {
  1259  	if conv == nil {
  1260  		return ""
  1261  	}
  1262  	var msg chat1.MessageUnboxed
  1263  	if fromMsg != nil {
  1264  		msg = *fromMsg
  1265  	} else if conv.Info.SnippetMsg != nil {
  1266  		msg = *conv.Info.SnippetMsg
  1267  	} else {
  1268  		return ""
  1269  	}
  1270  	if !msg.IsValid() {
  1271  		return ""
  1272  	}
  1273  
  1274  	mvalid := msg.Valid()
  1275  	if mvalid.IsEphemeral() {
  1276  		// If the message is already exploded, nothing to see here.
  1277  		if !msg.IsValidFull() {
  1278  			return ""
  1279  		}
  1280  		switch msg.GetMessageType() {
  1281  		case chat1.MessageType_TEXT, chat1.MessageType_ATTACHMENT, chat1.MessageType_EDIT:
  1282  			return "💣 exploding message."
  1283  		default:
  1284  			return ""
  1285  		}
  1286  	} else if plaintextDesktopDisabled {
  1287  		return "New message"
  1288  	}
  1289  
  1290  	switch msg.GetMessageType() {
  1291  	case chat1.MessageType_REACTION:
  1292  		reaction, err := GetReaction(msg)
  1293  		if err != nil {
  1294  			return ""
  1295  		}
  1296  		var prefix string
  1297  		if showSenderPrefix(*conv) {
  1298  			prefix = mvalid.SenderUsername + " "
  1299  		}
  1300  		return emoji.Sprintf("%sreacted to your message with %v", prefix, reaction)
  1301  	default:
  1302  		decoration, snippetBody, _ := GetMsgSnippet(ctx, g, uid, msg, *conv, currentUsername)
  1303  		return emoji.Sprintf("%s %s", decoration.ToEmoji(), snippetBody)
  1304  	}
  1305  }
  1306  
  1307  func StripUsernameFromConvName(name string, username string) (res string) {
  1308  	res = strings.ReplaceAll(name, fmt.Sprintf(",%s", username), "")
  1309  	res = strings.ReplaceAll(res, fmt.Sprintf("%s,", username), "")
  1310  	return res
  1311  }
  1312  
  1313  func PresentRemoteConversationAsSmallTeamRow(ctx context.Context, rc types.RemoteConversation,
  1314  	username string) (res chat1.UIInboxSmallTeamRow) {
  1315  	res.ConvID = rc.ConvIDStr
  1316  	res.IsTeam = rc.GetTeamType() != chat1.TeamType_NONE
  1317  	res.Name = StripUsernameFromConvName(GetRemoteConvDisplayName(rc), username)
  1318  	res.Time = GetConvMtime(rc)
  1319  	if rc.LocalMetadata != nil {
  1320  		res.SnippetDecoration = rc.LocalMetadata.SnippetDecoration
  1321  		res.Snippet = &rc.LocalMetadata.Snippet
  1322  	}
  1323  	res.Draft = rc.LocalDraft
  1324  	res.IsMuted = rc.Conv.Metadata.Status == chat1.ConversationStatus_MUTED
  1325  	return res
  1326  }
  1327  
  1328  func PresentRemoteConversationAsBigTeamChannelRow(ctx context.Context, rc types.RemoteConversation) (res chat1.UIInboxBigTeamChannelRow) {
  1329  	res.ConvID = rc.ConvIDStr
  1330  	res.Channelname = rc.GetTopicName()
  1331  	res.Teamname = GetRemoteConvTLFName(rc)
  1332  	res.Draft = rc.LocalDraft
  1333  	res.IsMuted = rc.Conv.Metadata.Status == chat1.ConversationStatus_MUTED
  1334  	return res
  1335  }
  1336  
  1337  func PresentRemoteConversation(ctx context.Context, g *globals.Context, uid gregor1.UID, rc types.RemoteConversation) (res chat1.UnverifiedInboxUIItem) {
  1338  	var tlfName string
  1339  	rawConv := rc.Conv
  1340  	latest, err := PickLatestMessageSummary(rawConv, nil)
  1341  	if err != nil {
  1342  		tlfName = ""
  1343  	} else {
  1344  		tlfName = latest.TlfName
  1345  	}
  1346  	res.ConvID = rc.ConvIDStr
  1347  	res.TlfID = rawConv.Metadata.IdTriple.Tlfid.TLFIDStr()
  1348  	res.TopicType = rawConv.GetTopicType()
  1349  	res.IsPublic = rawConv.Metadata.Visibility == keybase1.TLFVisibility_PUBLIC
  1350  	res.IsDefaultConv = rawConv.Metadata.IsDefaultConv
  1351  	res.Name = tlfName
  1352  	res.Status = rawConv.Metadata.Status
  1353  	res.Time = GetConvMtime(rc)
  1354  	res.Visibility = rawConv.Metadata.Visibility
  1355  	res.Notifications = rawConv.Notifications
  1356  	res.MembersType = rawConv.GetMembersType()
  1357  	res.MemberStatus = rawConv.ReaderInfo.Status
  1358  	res.TeamType = rawConv.Metadata.TeamType
  1359  	res.Version = rawConv.Metadata.Version
  1360  	res.LocalVersion = rawConv.Metadata.LocalVersion
  1361  	res.MaxMsgID = rawConv.ReaderInfo.MaxMsgid
  1362  	res.MaxVisibleMsgID = rawConv.MaxVisibleMsgID()
  1363  	res.ReadMsgID = rawConv.ReaderInfo.ReadMsgid
  1364  	res.Supersedes = rawConv.Metadata.Supersedes
  1365  	res.SupersededBy = rawConv.Metadata.SupersededBy
  1366  	res.FinalizeInfo = rawConv.Metadata.FinalizeInfo
  1367  	res.Commands =
  1368  		chat1.NewConversationCommandGroupsWithBuiltin(g.CommandsSource.GetBuiltinCommandType(ctx, rc))
  1369  	if rc.LocalMetadata != nil {
  1370  		res.LocalMetadata = &chat1.UnverifiedInboxUIItemMetadata{
  1371  			ChannelName: rc.LocalMetadata.TopicName,
  1372  			Headline:    rc.LocalMetadata.Headline,
  1373  			HeadlineDecorated: DecorateWithLinks(ctx,
  1374  				PresentDecoratedSnippet(ctx, g, rc.LocalMetadata.Headline, uid,
  1375  					chat1.MessageType_HEADLINE, rc.LocalMetadata.HeadlineEmojis)),
  1376  			Snippet:           rc.LocalMetadata.Snippet,
  1377  			SnippetDecoration: rc.LocalMetadata.SnippetDecoration,
  1378  			WriterNames:       rc.LocalMetadata.WriterNames,
  1379  			ResetParticipants: rc.LocalMetadata.ResetParticipants,
  1380  		}
  1381  		res.Name = rc.LocalMetadata.Name
  1382  	}
  1383  	res.ConvRetention = rawConv.ConvRetention
  1384  	res.TeamRetention = rawConv.TeamRetention
  1385  	res.Draft = rc.LocalDraft
  1386  	return res
  1387  }
  1388  
  1389  func PresentRemoteConversations(ctx context.Context, g *globals.Context, uid gregor1.UID, rcs []types.RemoteConversation) (res []chat1.UnverifiedInboxUIItem) {
  1390  	res = make([]chat1.UnverifiedInboxUIItem, 0, len(rcs))
  1391  	for _, rc := range rcs {
  1392  		res = append(res, PresentRemoteConversation(ctx, g, uid, rc))
  1393  	}
  1394  	return res
  1395  }
  1396  
  1397  func SearchableRemoteConversationName(conv types.RemoteConversation, username string) string {
  1398  	name := GetRemoteConvDisplayName(conv)
  1399  	return searchableRemoteConversationNameFromStr(name, username)
  1400  }
  1401  
  1402  func searchableRemoteConversationNameFromStr(name string, username string) string {
  1403  	// Check for self conv or big team conv
  1404  	if name == username || strings.Contains(name, "#") {
  1405  		return name
  1406  	}
  1407  
  1408  	name = strings.TrimPrefix(name, fmt.Sprintf("%s,", username))
  1409  	name = strings.TrimSuffix(name, fmt.Sprintf(",%s", username))
  1410  	name = strings.ReplaceAll(name, fmt.Sprintf(",%s,", username), ",")
  1411  	return name
  1412  }
  1413  
  1414  func PresentRemoteConversationAsSearchHit(conv types.RemoteConversation, username string) chat1.UIChatSearchConvHit {
  1415  	return chat1.UIChatSearchConvHit{
  1416  		ConvID:   conv.ConvIDStr,
  1417  		TeamType: conv.GetTeamType(),
  1418  		Name:     SearchableRemoteConversationName(conv, username),
  1419  		Mtime:    conv.GetMtime(),
  1420  	}
  1421  }
  1422  
  1423  func PresentRemoteConversationsAsSearchHits(convs []types.RemoteConversation, username string) (res []chat1.UIChatSearchConvHit) {
  1424  	res = make([]chat1.UIChatSearchConvHit, 0, len(convs))
  1425  	for _, c := range convs {
  1426  		res = append(res, PresentRemoteConversationAsSearchHit(c, username))
  1427  	}
  1428  	return res
  1429  }
  1430  
  1431  func PresentConversationErrorLocal(ctx context.Context, g *globals.Context, uid gregor1.UID, rawConv chat1.ConversationErrorLocal) (res chat1.InboxUIItemError) {
  1432  	res.Message = rawConv.Message
  1433  	res.RekeyInfo = rawConv.RekeyInfo
  1434  	res.RemoteConv = PresentRemoteConversation(ctx, g, uid, types.RemoteConversation{
  1435  		Conv:      rawConv.RemoteConv,
  1436  		ConvIDStr: rawConv.RemoteConv.GetConvID().ConvIDStr(),
  1437  	})
  1438  	res.Typ = rawConv.Typ
  1439  	res.UnverifiedTLFName = rawConv.UnverifiedTLFName
  1440  	return res
  1441  }
  1442  
  1443  func getParticipantType(username string) chat1.UIParticipantType {
  1444  	if strings.HasSuffix(username, "@phone") {
  1445  		return chat1.UIParticipantType_PHONENO
  1446  	}
  1447  	if strings.HasSuffix(username, "@email") {
  1448  		return chat1.UIParticipantType_EMAIL
  1449  	}
  1450  	return chat1.UIParticipantType_USER
  1451  }
  1452  
  1453  func PresentConversationParticipantsLocal(ctx context.Context, rawParticipants []chat1.ConversationLocalParticipant) (participants []chat1.UIParticipant) {
  1454  	participants = make([]chat1.UIParticipant, 0, len(rawParticipants))
  1455  	for _, p := range rawParticipants {
  1456  		participantType := getParticipantType(p.Username)
  1457  		participants = append(participants, chat1.UIParticipant{
  1458  			Assertion:   p.Username,
  1459  			InConvName:  p.InConvName,
  1460  			ContactName: p.ContactName,
  1461  			FullName:    p.Fullname,
  1462  			Type:        participantType,
  1463  		})
  1464  	}
  1465  	return participants
  1466  }
  1467  
  1468  type PresentParticipantsMode int
  1469  
  1470  const (
  1471  	PresentParticipantsModeInclude PresentParticipantsMode = iota
  1472  	PresentParticipantsModeSkip
  1473  )
  1474  
  1475  func PresentConversationLocal(ctx context.Context, g *globals.Context, uid gregor1.UID,
  1476  	rawConv chat1.ConversationLocal, partMode PresentParticipantsMode) (res chat1.InboxUIItem) {
  1477  	res.ConvID = rawConv.GetConvID().ConvIDStr()
  1478  	res.TlfID = rawConv.Info.Triple.Tlfid.TLFIDStr()
  1479  	res.TopicType = rawConv.GetTopicType()
  1480  	res.IsPublic = rawConv.Info.Visibility == keybase1.TLFVisibility_PUBLIC
  1481  	res.IsDefaultConv = rawConv.Info.IsDefaultConv
  1482  	res.Name = rawConv.Info.TlfName
  1483  	res.SnippetDecoration, res.Snippet, res.SnippetDecorated =
  1484  		GetConvSnippet(ctx, g, uid, rawConv, g.GetEnv().GetUsername().String())
  1485  	res.Channel = rawConv.Info.TopicName
  1486  	res.Headline = rawConv.Info.Headline
  1487  	res.HeadlineDecorated = DecorateWithLinks(ctx, PresentDecoratedSnippet(ctx, g, rawConv.Info.Headline, uid,
  1488  		chat1.MessageType_HEADLINE, rawConv.Info.HeadlineEmojis))
  1489  	res.ResetParticipants = rawConv.Info.ResetNames
  1490  	res.Status = rawConv.Info.Status
  1491  	res.MembersType = rawConv.GetMembersType()
  1492  	res.MemberStatus = rawConv.Info.MemberStatus
  1493  	res.Visibility = rawConv.Info.Visibility
  1494  	res.Time = GetConvMtimeLocal(rawConv)
  1495  	res.FinalizeInfo = rawConv.GetFinalizeInfo()
  1496  	res.SupersededBy = rawConv.SupersededBy
  1497  	res.Supersedes = rawConv.Supersedes
  1498  	res.IsEmpty = rawConv.IsEmpty
  1499  	res.Notifications = rawConv.Notifications
  1500  	res.CreatorInfo = rawConv.CreatorInfo
  1501  	res.TeamType = rawConv.Info.TeamType
  1502  	res.Version = rawConv.Info.Version
  1503  	res.LocalVersion = rawConv.Info.LocalVersion
  1504  	res.MaxMsgID = rawConv.ReaderInfo.MaxMsgid
  1505  	res.MaxVisibleMsgID = rawConv.MaxVisibleMsgID()
  1506  	res.ReadMsgID = rawConv.ReaderInfo.ReadMsgid
  1507  	res.ConvRetention = rawConv.ConvRetention
  1508  	res.TeamRetention = rawConv.TeamRetention
  1509  	res.ConvSettings = rawConv.ConvSettings
  1510  	res.Commands = rawConv.Commands
  1511  	res.BotCommands = rawConv.BotCommands
  1512  	res.BotAliases = rawConv.BotAliases
  1513  	res.Draft = rawConv.Info.Draft
  1514  	if rawConv.Info.PinnedMsg != nil {
  1515  		res.PinnedMsg = new(chat1.UIPinnedMessage)
  1516  		res.PinnedMsg.Message = PresentMessageUnboxed(ctx, g, rawConv.Info.PinnedMsg.Message, uid,
  1517  			rawConv.GetConvID())
  1518  		res.PinnedMsg.PinnerUsername = rawConv.Info.PinnedMsg.PinnerUsername
  1519  	}
  1520  	switch partMode {
  1521  	case PresentParticipantsModeInclude:
  1522  		res.Participants = PresentConversationParticipantsLocal(ctx, rawConv.Info.Participants)
  1523  	default:
  1524  	}
  1525  	return res
  1526  }
  1527  
  1528  func PresentConversationLocals(ctx context.Context, g *globals.Context, uid gregor1.UID,
  1529  	convs []chat1.ConversationLocal, partMode PresentParticipantsMode) (res []chat1.InboxUIItem) {
  1530  	res = make([]chat1.InboxUIItem, 0, len(convs))
  1531  	for _, conv := range convs {
  1532  		res = append(res, PresentConversationLocal(ctx, g, uid, conv, partMode))
  1533  	}
  1534  	return res
  1535  }
  1536  
  1537  func PresentThreadView(ctx context.Context, g *globals.Context, uid gregor1.UID, tv chat1.ThreadView,
  1538  	convID chat1.ConversationID) (res chat1.UIMessages) {
  1539  	res.Pagination = PresentPagination(tv.Pagination)
  1540  	res.Messages = make([]chat1.UIMessage, 0, len(tv.Messages))
  1541  	for _, msg := range tv.Messages {
  1542  		res.Messages = append(res.Messages, PresentMessageUnboxed(ctx, g, msg, uid, convID))
  1543  	}
  1544  	return res
  1545  }
  1546  
  1547  func computeOutboxOrdinal(obr chat1.OutboxRecord) float64 {
  1548  	return computeOrdinal(obr.Msg.ClientHeader.OutboxInfo.Prev, obr.Ordinal)
  1549  }
  1550  
  1551  // Compute an "ordinal". There are two senses of "ordinal".
  1552  // The service considers ordinals ints, like 3, which are the offset after some message ID.
  1553  // The frontend considers ordinals floats like "180.03" where before the dot is
  1554  // a message ID, and after the dot is a sub-position in thousandths.
  1555  // This function translates from the service's sense to the frontend's sense.
  1556  func computeOrdinal(messageID chat1.MessageID, serviceOrdinal int) (frontendOrdinal float64) {
  1557  	return float64(messageID) + float64(serviceOrdinal)/1000.0
  1558  }
  1559  
  1560  func PresentChannelNameMentions(ctx context.Context, crs []chat1.ChannelNameMention) (res []chat1.UIChannelNameMention) {
  1561  	res = make([]chat1.UIChannelNameMention, 0, len(crs))
  1562  	for _, cr := range crs {
  1563  		res = append(res, chat1.UIChannelNameMention{
  1564  			Name:   cr.TopicName,
  1565  			ConvID: cr.ConvID.ConvIDStr(),
  1566  		})
  1567  	}
  1568  	return res
  1569  }
  1570  
  1571  func formatVideoDuration(ms int) string {
  1572  	s := ms / 1000
  1573  	// see if we have hours
  1574  	if s >= 3600 {
  1575  		hours := s / 3600
  1576  		minutes := (s % 3600) / 60
  1577  		seconds := s - (hours*3600 + minutes*60)
  1578  		return fmt.Sprintf("%d:%02d:%02d", hours, minutes, seconds)
  1579  	}
  1580  	minutes := s / 60
  1581  	seconds := s % 60
  1582  	return fmt.Sprintf("%d:%02d", minutes, seconds)
  1583  }
  1584  
  1585  func PresentBytes(bytes int64) string {
  1586  	const (
  1587  		BYTE = 1.0 << (10 * iota)
  1588  		KILOBYTE
  1589  		MEGABYTE
  1590  		GIGABYTE
  1591  		TERABYTE
  1592  	)
  1593  	unit := ""
  1594  	value := float64(bytes)
  1595  	switch {
  1596  	case bytes >= TERABYTE:
  1597  		unit = "TB"
  1598  		value /= TERABYTE
  1599  	case bytes >= GIGABYTE:
  1600  		unit = "GB"
  1601  		value /= GIGABYTE
  1602  	case bytes >= MEGABYTE:
  1603  		unit = "MB"
  1604  		value /= MEGABYTE
  1605  	case bytes >= KILOBYTE:
  1606  		unit = "KB"
  1607  		value /= KILOBYTE
  1608  	case bytes >= BYTE:
  1609  		unit = "B"
  1610  	case bytes == 0:
  1611  		return "0"
  1612  	}
  1613  	return fmt.Sprintf("%.02f%s", value, unit)
  1614  }
  1615  
  1616  func formatVideoSize(bytes int64) string {
  1617  	return PresentBytes(bytes)
  1618  }
  1619  
  1620  func presentAttachmentAssetInfo(ctx context.Context, g *globals.Context, msg chat1.MessageUnboxed,
  1621  	convID chat1.ConversationID) *chat1.UIAssetUrlInfo {
  1622  	body := msg.Valid().MessageBody
  1623  	typ, err := body.MessageType()
  1624  	if err != nil {
  1625  		return nil
  1626  	}
  1627  	switch typ {
  1628  	case chat1.MessageType_ATTACHMENT, chat1.MessageType_ATTACHMENTUPLOADED:
  1629  		var hasFullURL, hasPreviewURL bool
  1630  		var asset chat1.Asset
  1631  		var info chat1.UIAssetUrlInfo
  1632  		if typ == chat1.MessageType_ATTACHMENT {
  1633  			asset = body.Attachment().Object
  1634  			info.MimeType = asset.MimeType
  1635  			hasFullURL = asset.Path != ""
  1636  			hasPreviewURL = body.Attachment().Preview != nil &&
  1637  				body.Attachment().Preview.Path != ""
  1638  		} else {
  1639  			asset = body.Attachmentuploaded().Object
  1640  			info.MimeType = asset.MimeType
  1641  			hasFullURL = asset.Path != ""
  1642  			hasPreviewURL = len(body.Attachmentuploaded().Previews) > 0 &&
  1643  				body.Attachmentuploaded().Previews[0].Path != ""
  1644  		}
  1645  		if hasFullURL {
  1646  			var cached bool
  1647  			info.FullUrl = g.AttachmentURLSrv.GetURL(ctx, convID, msg.GetMessageID(), false, false, false)
  1648  			cached, err = g.AttachmentURLSrv.GetAttachmentFetcher().IsAssetLocal(ctx, asset)
  1649  			if err != nil {
  1650  				cached = false
  1651  			}
  1652  			info.FullUrlCached = cached
  1653  		}
  1654  		if hasPreviewURL {
  1655  			info.PreviewUrl = g.AttachmentURLSrv.GetURL(ctx, convID, msg.GetMessageID(), true, false, false)
  1656  		}
  1657  		atyp, err := asset.Metadata.AssetType()
  1658  		if err == nil && atyp == chat1.AssetMetadataType_VIDEO && strings.HasPrefix(info.MimeType, "video") {
  1659  			if asset.Metadata.Video().DurationMs > 1 {
  1660  				info.VideoDuration = new(string)
  1661  				*info.VideoDuration = formatVideoDuration(asset.Metadata.Video().DurationMs) + ", " +
  1662  					formatVideoSize(asset.Size)
  1663  			}
  1664  			info.InlineVideoPlayable = true
  1665  		}
  1666  		if info.FullUrl == "" && info.PreviewUrl == "" && info.MimeType == "" {
  1667  			return nil
  1668  		}
  1669  		return &info
  1670  	}
  1671  	return nil
  1672  }
  1673  
  1674  func presentPaymentInfo(ctx context.Context, g *globals.Context, msgID chat1.MessageID,
  1675  	convID chat1.ConversationID, msg chat1.MessageUnboxedValid) []chat1.UIPaymentInfo {
  1676  	typ, err := msg.MessageBody.MessageType()
  1677  	if err != nil {
  1678  		return nil
  1679  	}
  1680  	var infos []chat1.UIPaymentInfo
  1681  	switch typ {
  1682  	case chat1.MessageType_SENDPAYMENT:
  1683  		body := msg.MessageBody.Sendpayment()
  1684  		info := g.StellarLoader.LoadPayment(ctx, convID, msgID, msg.SenderUsername, body.PaymentID)
  1685  		if info != nil {
  1686  			infos = []chat1.UIPaymentInfo{*info}
  1687  		}
  1688  	case chat1.MessageType_TEXT:
  1689  		body := msg.MessageBody.Text()
  1690  		// load any payments that were in the body of the text message
  1691  		for _, payment := range body.Payments {
  1692  			rtyp, err := payment.Result.ResultTyp()
  1693  			if err != nil {
  1694  				continue
  1695  			}
  1696  			switch rtyp {
  1697  			case chat1.TextPaymentResultTyp_SENT:
  1698  				paymentID := payment.Result.Sent()
  1699  				info := g.StellarLoader.LoadPayment(ctx, convID, msgID, msg.SenderUsername, paymentID)
  1700  				if info != nil {
  1701  					infos = append(infos, *info)
  1702  				}
  1703  			default:
  1704  				// Nothing to do for other payment result types.
  1705  			}
  1706  		}
  1707  	}
  1708  	for index := range infos {
  1709  		infos[index].Note = EscapeForDecorate(ctx, infos[index].Note)
  1710  	}
  1711  	return infos
  1712  }
  1713  
  1714  func presentRequestInfo(ctx context.Context, g *globals.Context, msgID chat1.MessageID,
  1715  	convID chat1.ConversationID, msg chat1.MessageUnboxedValid) *chat1.UIRequestInfo {
  1716  
  1717  	typ, err := msg.MessageBody.MessageType()
  1718  	if err != nil {
  1719  		return nil
  1720  	}
  1721  	switch typ {
  1722  	case chat1.MessageType_REQUESTPAYMENT:
  1723  		body := msg.MessageBody.Requestpayment()
  1724  		return g.StellarLoader.LoadRequest(ctx, convID, msgID, msg.SenderUsername, body.RequestID)
  1725  	default:
  1726  		// Nothing to do for other message types.
  1727  	}
  1728  	return nil
  1729  }
  1730  
  1731  func PresentUnfurl(ctx context.Context, g *globals.Context, convID chat1.ConversationID, u chat1.Unfurl) *chat1.UnfurlDisplay {
  1732  	ud, err := display.DisplayUnfurl(ctx, g.AttachmentURLSrv, convID, u)
  1733  	if err != nil {
  1734  		g.GetLog().CDebugf(ctx, "PresentUnfurl: failed to display unfurl: %s", err)
  1735  		return nil
  1736  	}
  1737  	return &ud
  1738  }
  1739  
  1740  func PresentUnfurls(ctx context.Context, g *globals.Context, uid gregor1.UID,
  1741  	convID chat1.ConversationID, unfurls map[chat1.MessageID]chat1.UnfurlResult) (res []chat1.UIMessageUnfurlInfo) {
  1742  	collapses := NewCollapses(g)
  1743  	res = make([]chat1.UIMessageUnfurlInfo, 0, len(unfurls))
  1744  	for unfurlMessageID, u := range unfurls {
  1745  		ud := PresentUnfurl(ctx, g, convID, u.Unfurl)
  1746  		if ud != nil {
  1747  			res = append(res, chat1.UIMessageUnfurlInfo{
  1748  				IsCollapsed: collapses.IsCollapsed(ctx, uid, convID, unfurlMessageID,
  1749  					chat1.MessageType_UNFURL),
  1750  				Unfurl:          *ud,
  1751  				UnfurlMessageID: unfurlMessageID,
  1752  				Url:             u.Url,
  1753  			})
  1754  		}
  1755  	}
  1756  	return res
  1757  }
  1758  
  1759  func PresentDecoratedReactionMap(ctx context.Context, g *globals.Context, uid gregor1.UID,
  1760  	convID chat1.ConversationID, msg chat1.MessageUnboxedValid, reactions chat1.ReactionMap) (res chat1.UIReactionMap) {
  1761  	shouldDecorate := len(msg.Emojis) > 0
  1762  	res.Reactions = make(map[string]chat1.UIReactionDesc, len(reactions.Reactions))
  1763  	for key, value := range reactions.Reactions {
  1764  		var desc chat1.UIReactionDesc
  1765  		if shouldDecorate {
  1766  			desc.Decorated = g.EmojiSource.Decorate(ctx, key, uid,
  1767  				chat1.MessageType_REACTION, msg.Emojis)
  1768  		}
  1769  		desc.Users = make(map[string]chat1.Reaction)
  1770  		for username, reaction := range value {
  1771  			desc.Users[username] = reaction
  1772  		}
  1773  		res.Reactions[key] = desc
  1774  	}
  1775  	return res
  1776  }
  1777  
  1778  func PresentDecoratedUserBio(ctx context.Context, bio string) (res string) {
  1779  	res = EscapeForDecorate(ctx, bio)
  1780  	res = EscapeShrugs(ctx, res)
  1781  	res = DecorateWithLinks(ctx, res)
  1782  	return res
  1783  }
  1784  
  1785  func systemMsgPresentText(ctx context.Context, uid gregor1.UID, msg chat1.MessageUnboxedValid) string {
  1786  	if !msg.MessageBody.IsType(chat1.MessageType_SYSTEM) {
  1787  		return ""
  1788  	}
  1789  	sysMsg := msg.MessageBody.System()
  1790  	typ, err := sysMsg.SystemType()
  1791  	if err != nil {
  1792  		return ""
  1793  	}
  1794  	switch typ {
  1795  	case chat1.MessageSystemType_NEWCHANNEL:
  1796  		var author string
  1797  		if uid.Eq(msg.ClientHeader.Sender) {
  1798  			author = "You "
  1799  		}
  1800  		if len(msg.ChannelNameMentions) == 1 {
  1801  			return fmt.Sprintf("%screated a new channel #%s", author, msg.ChannelNameMentions[0].TopicName)
  1802  		} else if len(msg.ChannelNameMentions) > 1 {
  1803  			return fmt.Sprintf("%screated #%s and %d other new channels", author, msg.ChannelNameMentions[0].TopicName, len(sysMsg.Newchannel().ConvIDs)-1)
  1804  		}
  1805  	default:
  1806  	}
  1807  	return ""
  1808  }
  1809  
  1810  func PresentDecoratedTextNoMentions(ctx context.Context, body string) string {
  1811  	// escape before applying xforms
  1812  	body = EscapeForDecorate(ctx, body)
  1813  	body = EscapeShrugs(ctx, body)
  1814  
  1815  	// This needs to happen before (deep) links.
  1816  	kbfsPaths := ParseKBFSPaths(ctx, body)
  1817  	body = DecorateWithKBFSPath(ctx, body, kbfsPaths)
  1818  
  1819  	// Links
  1820  	body = DecorateWithLinks(ctx, body)
  1821  	return body
  1822  }
  1823  
  1824  func PresentDecoratedSnippet(ctx context.Context, g *globals.Context, body string,
  1825  	uid gregor1.UID, msgType chat1.MessageType, emojis []chat1.HarvestedEmoji) string {
  1826  	body = EscapeForDecorate(ctx, body)
  1827  	body = EscapeShrugs(ctx, body)
  1828  	return g.EmojiSource.Decorate(ctx, body, uid, msgType, emojis)
  1829  }
  1830  
  1831  func PresentDecoratedPendingTextBody(ctx context.Context, g *globals.Context, uid gregor1.UID,
  1832  	msg chat1.MessagePlaintext) *string {
  1833  	typ, err := msg.MessageBody.MessageType()
  1834  	if err != nil {
  1835  		return nil
  1836  	}
  1837  	body := msg.MessageBody.TextForDecoration()
  1838  	body = PresentDecoratedTextNoMentions(ctx, body)
  1839  	body = g.EmojiSource.Decorate(ctx, body, uid, typ, msg.Emojis)
  1840  	return &body
  1841  }
  1842  
  1843  func PresentDecoratedTextBody(ctx context.Context, g *globals.Context, uid gregor1.UID,
  1844  	convID chat1.ConversationID, msg chat1.MessageUnboxedValid) *string {
  1845  	msgBody := msg.MessageBody
  1846  	typ, err := msgBody.MessageType()
  1847  	if err != nil {
  1848  		return nil
  1849  	}
  1850  	body := msgBody.TextForDecoration()
  1851  	if len(body) == 0 {
  1852  		return nil
  1853  	}
  1854  	var payments []chat1.TextPayment
  1855  	switch typ {
  1856  	case chat1.MessageType_TEXT:
  1857  		payments = msgBody.Text().Payments
  1858  	case chat1.MessageType_SYSTEM:
  1859  		body = systemMsgPresentText(ctx, uid, msg)
  1860  	}
  1861  
  1862  	body = PresentDecoratedTextNoMentions(ctx, body)
  1863  	// Payments
  1864  	body = g.StellarSender.DecorateWithPayments(ctx, body, payments)
  1865  	// Emojis
  1866  	body = g.EmojiSource.Decorate(ctx, body, uid, typ, msg.Emojis)
  1867  	// Mentions
  1868  	body = DecorateWithMentions(ctx, body, msg.AtMentionUsernames, msg.MaybeMentions, msg.ChannelMention,
  1869  		msg.ChannelNameMentions)
  1870  	return &body
  1871  }
  1872  
  1873  func loadTeamMentions(ctx context.Context, g *globals.Context, uid gregor1.UID,
  1874  	valid chat1.MessageUnboxedValid) {
  1875  	var knownTeamMentions []chat1.KnownTeamMention
  1876  	typ, err := valid.MessageBody.MessageType()
  1877  	if err != nil {
  1878  		return
  1879  	}
  1880  	switch typ {
  1881  	case chat1.MessageType_TEXT:
  1882  		knownTeamMentions = valid.MessageBody.Text().TeamMentions
  1883  	case chat1.MessageType_FLIP:
  1884  		knownTeamMentions = valid.MessageBody.Flip().TeamMentions
  1885  	case chat1.MessageType_EDIT:
  1886  		knownTeamMentions = valid.MessageBody.Edit().TeamMentions
  1887  	}
  1888  	for _, tm := range valid.MaybeMentions {
  1889  		if err := g.TeamMentionLoader.LoadTeamMention(ctx, uid, tm, knownTeamMentions, false); err != nil {
  1890  			g.GetLog().CDebugf(ctx, "loadTeamMentions: error loading team mentions: %+v", err)
  1891  		}
  1892  	}
  1893  }
  1894  
  1895  func presentFlipGameID(ctx context.Context, g *globals.Context, uid gregor1.UID,
  1896  	convID chat1.ConversationID, msg chat1.MessageUnboxed) *chat1.FlipGameIDStr {
  1897  	typ, err := msg.State()
  1898  	if err != nil {
  1899  		return nil
  1900  	}
  1901  	var body chat1.MessageBody
  1902  	switch typ {
  1903  	case chat1.MessageUnboxedState_VALID:
  1904  		body = msg.Valid().MessageBody
  1905  	case chat1.MessageUnboxedState_OUTBOX:
  1906  		body = msg.Outbox().Msg.MessageBody
  1907  	default:
  1908  		return nil
  1909  	}
  1910  	if !body.IsType(chat1.MessageType_FLIP) {
  1911  		return nil
  1912  	}
  1913  	if msg.GetTopicType() == chat1.TopicType_CHAT && !msg.IsOutbox() {
  1914  		// only queue up a flip load for the flip messages in chat channels
  1915  		g.CoinFlipManager.LoadFlip(ctx, uid, convID, msg.GetMessageID(), body.Flip().FlipConvID,
  1916  			body.Flip().GameID)
  1917  	}
  1918  	ret := body.Flip().GameID.FlipGameIDStr()
  1919  	return &ret
  1920  }
  1921  
  1922  func PresentMessagesUnboxed(ctx context.Context, g *globals.Context, msgs []chat1.MessageUnboxed,
  1923  	uid gregor1.UID, convID chat1.ConversationID) (res []chat1.UIMessage) {
  1924  	res = make([]chat1.UIMessage, 0, len(msgs))
  1925  	for _, msg := range msgs {
  1926  		res = append(res, PresentMessageUnboxed(ctx, g, msg, uid, convID))
  1927  	}
  1928  	return res
  1929  }
  1930  
  1931  func PresentMessageUnboxed(ctx context.Context, g *globals.Context, rawMsg chat1.MessageUnboxed,
  1932  	uid gregor1.UID, convID chat1.ConversationID) (res chat1.UIMessage) {
  1933  	miscErr := func(err error) chat1.UIMessage {
  1934  		return chat1.NewUIMessageWithError(chat1.MessageUnboxedError{
  1935  			ErrType:   chat1.MessageUnboxedErrorType_MISC,
  1936  			ErrMsg:    err.Error(),
  1937  			MessageID: rawMsg.GetMessageID(),
  1938  		})
  1939  	}
  1940  
  1941  	collapses := NewCollapses(g)
  1942  	state, err := rawMsg.State()
  1943  	if err != nil {
  1944  		return miscErr(err)
  1945  	}
  1946  	switch state {
  1947  	case chat1.MessageUnboxedState_VALID:
  1948  		valid := rawMsg.Valid()
  1949  		if !rawMsg.IsValidFull() {
  1950  			// If we have an expired ephemeral message, don't show an error
  1951  			// message.
  1952  			if !(valid.IsEphemeral() && valid.IsEphemeralExpired(time.Now())) {
  1953  				return miscErr(fmt.Errorf("unexpected deleted %v message",
  1954  					strings.ToLower(rawMsg.GetMessageType().String())))
  1955  			}
  1956  		}
  1957  		var strOutboxID *string
  1958  		if valid.ClientHeader.OutboxID != nil {
  1959  			so := valid.ClientHeader.OutboxID.String()
  1960  			strOutboxID = &so
  1961  		}
  1962  		var replyTo *chat1.UIMessage
  1963  		if valid.ReplyTo != nil {
  1964  			replyTo = new(chat1.UIMessage)
  1965  			*replyTo = PresentMessageUnboxed(ctx, g, *valid.ReplyTo, uid, convID)
  1966  		}
  1967  		var pinnedMessageID *chat1.MessageID
  1968  		if valid.MessageBody.IsType(chat1.MessageType_PIN) {
  1969  			pinnedMessageID = new(chat1.MessageID)
  1970  			*pinnedMessageID = valid.MessageBody.Pin().MsgID
  1971  		}
  1972  		loadTeamMentions(ctx, g, uid, valid)
  1973  		bodySummary, _ := GetMsgSnippetBody(ctx, g, uid, convID, rawMsg)
  1974  		res = chat1.NewUIMessageWithValid(chat1.UIMessageValid{
  1975  			MessageID:             rawMsg.GetMessageID(),
  1976  			Ctime:                 valid.ServerHeader.Ctime,
  1977  			OutboxID:              strOutboxID,
  1978  			MessageBody:           valid.MessageBody,
  1979  			DecoratedTextBody:     PresentDecoratedTextBody(ctx, g, uid, convID, valid),
  1980  			BodySummary:           bodySummary,
  1981  			SenderUsername:        valid.SenderUsername,
  1982  			SenderDeviceName:      valid.SenderDeviceName,
  1983  			SenderDeviceType:      valid.SenderDeviceType,
  1984  			SenderDeviceRevokedAt: valid.SenderDeviceRevokedAt,
  1985  			SenderUID:             valid.ClientHeader.Sender,
  1986  			SenderDeviceID:        valid.ClientHeader.SenderDevice,
  1987  			Superseded:            valid.ServerHeader.SupersededBy != 0,
  1988  			AtMentions:            valid.AtMentionUsernames,
  1989  			ChannelMention:        valid.ChannelMention,
  1990  			ChannelNameMentions:   PresentChannelNameMentions(ctx, valid.ChannelNameMentions),
  1991  			AssetUrlInfo:          presentAttachmentAssetInfo(ctx, g, rawMsg, convID),
  1992  			IsEphemeral:           valid.IsEphemeral(),
  1993  			IsEphemeralExpired:    valid.IsEphemeralExpired(time.Now()),
  1994  			ExplodedBy:            valid.ExplodedBy(),
  1995  			Etime:                 valid.Etime(),
  1996  			Reactions:             PresentDecoratedReactionMap(ctx, g, uid, convID, valid, valid.Reactions),
  1997  			HasPairwiseMacs:       valid.HasPairwiseMacs(),
  1998  			FlipGameID:            presentFlipGameID(ctx, g, uid, convID, rawMsg),
  1999  			PaymentInfos:          presentPaymentInfo(ctx, g, rawMsg.GetMessageID(), convID, valid),
  2000  			RequestInfo:           presentRequestInfo(ctx, g, rawMsg.GetMessageID(), convID, valid),
  2001  			Unfurls:               PresentUnfurls(ctx, g, uid, convID, valid.Unfurls),
  2002  			IsDeleteable:          IsDeleteableByDeleteMessageType(valid),
  2003  			IsEditable:            IsEditableByEditMessageType(rawMsg.GetMessageType()),
  2004  			ReplyTo:               replyTo,
  2005  			PinnedMessageID:       pinnedMessageID,
  2006  			BotUsername:           valid.BotUsername,
  2007  			IsCollapsed: collapses.IsCollapsed(ctx, uid, convID, rawMsg.GetMessageID(),
  2008  				rawMsg.GetMessageType()),
  2009  		})
  2010  	case chat1.MessageUnboxedState_OUTBOX:
  2011  		var body, title, filename string
  2012  		var decoratedBody *string
  2013  		var preview *chat1.MakePreviewRes
  2014  		typ := rawMsg.Outbox().Msg.ClientHeader.MessageType
  2015  		switch typ {
  2016  		case chat1.MessageType_TEXT:
  2017  			body = rawMsg.Outbox().Msg.MessageBody.Text().Body
  2018  			decoratedBody = PresentDecoratedPendingTextBody(ctx, g, uid, rawMsg.Outbox().Msg)
  2019  		case chat1.MessageType_FLIP:
  2020  			body = rawMsg.Outbox().Msg.MessageBody.Flip().Text
  2021  			decoratedBody = new(string)
  2022  			*decoratedBody = EscapeShrugs(ctx, body)
  2023  		case chat1.MessageType_EDIT:
  2024  			body = rawMsg.Outbox().Msg.MessageBody.Edit().Body
  2025  		case chat1.MessageType_ATTACHMENT:
  2026  			preview = rawMsg.Outbox().Preview
  2027  			msgBody := rawMsg.Outbox().Msg.MessageBody
  2028  			btyp, err := msgBody.MessageType()
  2029  			if err == nil && btyp == chat1.MessageType_ATTACHMENT {
  2030  				asset := msgBody.Attachment().Object
  2031  				title = asset.Title
  2032  				filename = asset.Filename
  2033  			}
  2034  		case chat1.MessageType_REACTION:
  2035  			body = rawMsg.Outbox().Msg.MessageBody.Reaction().Body
  2036  			decoratedBody = PresentDecoratedPendingTextBody(ctx, g, uid, rawMsg.Outbox().Msg)
  2037  		}
  2038  		var replyTo *chat1.UIMessage
  2039  		if rawMsg.Outbox().ReplyTo != nil {
  2040  			replyTo = new(chat1.UIMessage)
  2041  			*replyTo = PresentMessageUnboxed(ctx, g, *rawMsg.Outbox().ReplyTo, uid, convID)
  2042  		}
  2043  		res = chat1.NewUIMessageWithOutbox(chat1.UIMessageOutbox{
  2044  			State:             rawMsg.Outbox().State,
  2045  			OutboxID:          rawMsg.Outbox().OutboxID.String(),
  2046  			MessageType:       typ,
  2047  			Body:              body,
  2048  			DecoratedTextBody: decoratedBody,
  2049  			Ctime:             rawMsg.Outbox().Ctime,
  2050  			Ordinal:           computeOutboxOrdinal(rawMsg.Outbox()),
  2051  			Preview:           preview,
  2052  			Title:             title,
  2053  			Filename:          filename,
  2054  			IsEphemeral:       rawMsg.Outbox().Msg.IsEphemeral(),
  2055  			FlipGameID:        presentFlipGameID(ctx, g, uid, convID, rawMsg),
  2056  			ReplyTo:           replyTo,
  2057  			Supersedes:        rawMsg.Outbox().Msg.ClientHeader.Supersedes,
  2058  		})
  2059  	case chat1.MessageUnboxedState_ERROR:
  2060  		res = chat1.NewUIMessageWithError(rawMsg.Error())
  2061  	case chat1.MessageUnboxedState_PLACEHOLDER:
  2062  		res = chat1.NewUIMessageWithPlaceholder(rawMsg.Placeholder())
  2063  	case chat1.MessageUnboxedState_JOURNEYCARD:
  2064  		journeycard := rawMsg.Journeycard()
  2065  		res = chat1.NewUIMessageWithJourneycard(chat1.UIMessageJourneycard{
  2066  			Ordinal:        computeOrdinal(journeycard.PrevID, journeycard.Ordinal),
  2067  			CardType:       journeycard.CardType,
  2068  			HighlightMsgID: journeycard.HighlightMsgID,
  2069  			OpenTeam:       journeycard.OpenTeam,
  2070  		})
  2071  	default:
  2072  		g.MetaContext(ctx).Debug("PresentMessageUnboxed: unhandled MessageUnboxedState: %v", state)
  2073  		// res = zero values
  2074  	}
  2075  	return res
  2076  }
  2077  
  2078  func PresentPagination(p *chat1.Pagination) (res *chat1.UIPagination) {
  2079  	if p == nil {
  2080  		return nil
  2081  	}
  2082  	res = new(chat1.UIPagination)
  2083  	res.Last = p.Last
  2084  	res.Num = p.Num
  2085  	res.Next = hex.EncodeToString(p.Next)
  2086  	res.Previous = hex.EncodeToString(p.Previous)
  2087  	return res
  2088  }
  2089  
  2090  func DecodePagination(p *chat1.UIPagination) (res *chat1.Pagination, err error) {
  2091  	if p == nil {
  2092  		return nil, nil
  2093  	}
  2094  	res = new(chat1.Pagination)
  2095  	res.Last = p.Last
  2096  	res.Num = p.Num
  2097  	if res.Next, err = hex.DecodeString(p.Next); err != nil {
  2098  		return nil, err
  2099  	}
  2100  	if res.Previous, err = hex.DecodeString(p.Previous); err != nil {
  2101  		return nil, err
  2102  	}
  2103  	return res, nil
  2104  }
  2105  
  2106  type ConvLocalByConvID []chat1.ConversationLocal
  2107  
  2108  func (c ConvLocalByConvID) Len() int      { return len(c) }
  2109  func (c ConvLocalByConvID) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
  2110  func (c ConvLocalByConvID) Less(i, j int) bool {
  2111  	return c[i].GetConvID().Less(c[j].GetConvID())
  2112  }
  2113  
  2114  type ConvByConvID []chat1.Conversation
  2115  
  2116  func (c ConvByConvID) Len() int      { return len(c) }
  2117  func (c ConvByConvID) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
  2118  func (c ConvByConvID) Less(i, j int) bool {
  2119  	return c[i].GetConvID().Less(c[j].GetConvID())
  2120  }
  2121  
  2122  type RemoteConvByConvID []types.RemoteConversation
  2123  
  2124  func (c RemoteConvByConvID) Len() int      { return len(c) }
  2125  func (c RemoteConvByConvID) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
  2126  func (c RemoteConvByConvID) Less(i, j int) bool {
  2127  	return c[i].GetConvID().Less(c[j].GetConvID())
  2128  }
  2129  
  2130  type RemoteConvByMtime []types.RemoteConversation
  2131  
  2132  func (c RemoteConvByMtime) Len() int      { return len(c) }
  2133  func (c RemoteConvByMtime) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
  2134  func (c RemoteConvByMtime) Less(i, j int) bool {
  2135  	return GetConvMtime(c[i]) > GetConvMtime(c[j])
  2136  }
  2137  
  2138  type ConvLocalByTopicName []chat1.ConversationLocal
  2139  
  2140  func (c ConvLocalByTopicName) Len() int      { return len(c) }
  2141  func (c ConvLocalByTopicName) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
  2142  func (c ConvLocalByTopicName) Less(i, j int) bool {
  2143  	return c[i].Info.TopicName < c[j].Info.TopicName
  2144  }
  2145  
  2146  type ByConvID []chat1.ConversationID
  2147  
  2148  func (c ByConvID) Len() int      { return len(c) }
  2149  func (c ByConvID) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
  2150  func (c ByConvID) Less(i, j int) bool {
  2151  	return c[i].Less(c[j])
  2152  }
  2153  
  2154  type ByMsgSummaryCtime []chat1.MessageSummary
  2155  
  2156  func (c ByMsgSummaryCtime) Len() int      { return len(c) }
  2157  func (c ByMsgSummaryCtime) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
  2158  func (c ByMsgSummaryCtime) Less(i, j int) bool {
  2159  	return c[i].Ctime.Before(c[j].Ctime)
  2160  }
  2161  
  2162  type ByMsgUnboxedCtime []chat1.MessageUnboxed
  2163  
  2164  func (c ByMsgUnboxedCtime) Len() int      { return len(c) }
  2165  func (c ByMsgUnboxedCtime) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
  2166  func (c ByMsgUnboxedCtime) Less(i, j int) bool {
  2167  	return c[i].Valid().ServerHeader.Ctime.Before(c[j].Valid().ServerHeader.Ctime)
  2168  }
  2169  
  2170  type ByMsgUnboxedMsgID []chat1.MessageUnboxed
  2171  
  2172  func (c ByMsgUnboxedMsgID) Len() int      { return len(c) }
  2173  func (c ByMsgUnboxedMsgID) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
  2174  func (c ByMsgUnboxedMsgID) Less(i, j int) bool {
  2175  	return c[i].GetMessageID() > c[j].GetMessageID()
  2176  }
  2177  
  2178  type ByMsgID []chat1.MessageID
  2179  
  2180  func (m ByMsgID) Len() int           { return len(m) }
  2181  func (m ByMsgID) Swap(i, j int)      { m[i], m[j] = m[j], m[i] }
  2182  func (m ByMsgID) Less(i, j int) bool { return m[i] > m[j] }
  2183  
  2184  func NotificationInfoSet(settings *chat1.ConversationNotificationInfo,
  2185  	apptype keybase1.DeviceType,
  2186  	kind chat1.NotificationKind, enabled bool) {
  2187  	if settings.Settings == nil {
  2188  		settings.Settings = make(map[keybase1.DeviceType]map[chat1.NotificationKind]bool)
  2189  	}
  2190  	if settings.Settings[apptype] == nil {
  2191  		settings.Settings[apptype] = make(map[chat1.NotificationKind]bool)
  2192  	}
  2193  	settings.Settings[apptype][kind] = enabled
  2194  }
  2195  
  2196  func DecodeBase64(enc []byte) ([]byte, error) {
  2197  	if len(enc) == 0 {
  2198  		return enc, nil
  2199  	}
  2200  
  2201  	b := make([]byte, base64.StdEncoding.DecodedLen(len(enc)))
  2202  	n, err := base64.StdEncoding.Decode(b, enc)
  2203  	return b[:n], err
  2204  }
  2205  
  2206  func RemoteConv(conv chat1.Conversation) types.RemoteConversation {
  2207  	return types.RemoteConversation{
  2208  		Conv:      conv,
  2209  		ConvIDStr: conv.GetConvID().ConvIDStr(),
  2210  	}
  2211  }
  2212  
  2213  func RemoteConvs(convs []chat1.Conversation) (res []types.RemoteConversation) {
  2214  	res = make([]types.RemoteConversation, 0, len(convs))
  2215  	for _, conv := range convs {
  2216  		res = append(res, RemoteConv(conv))
  2217  	}
  2218  	return res
  2219  }
  2220  
  2221  func PluckConvs(rcs []types.RemoteConversation) (res []chat1.Conversation) {
  2222  	res = make([]chat1.Conversation, 0, len(rcs))
  2223  	for _, rc := range rcs {
  2224  		res = append(res, rc.Conv)
  2225  	}
  2226  	return res
  2227  }
  2228  
  2229  func SplitTLFName(tlfName string) []string {
  2230  	return strings.Split(strings.Fields(tlfName)[0], ",")
  2231  }
  2232  
  2233  func UsernamePackageToParticipant(p libkb.UsernamePackage) chat1.ConversationLocalParticipant {
  2234  	var fullName *string
  2235  	if p.FullName != nil {
  2236  		s := string(p.FullName.FullName)
  2237  		fullName = &s
  2238  	}
  2239  	return chat1.ConversationLocalParticipant{
  2240  		Username: p.NormalizedUsername.String(),
  2241  		Fullname: fullName,
  2242  	}
  2243  }
  2244  
  2245  type pagerMsg struct {
  2246  	msgID chat1.MessageID
  2247  }
  2248  
  2249  func (p pagerMsg) GetMessageID() chat1.MessageID {
  2250  	return p.msgID
  2251  }
  2252  
  2253  func MessageIDControlToPagination(ctx context.Context, logger DebugLabeler, control *chat1.MessageIDControl,
  2254  	conv *types.RemoteConversation) (res *chat1.Pagination) {
  2255  	if control == nil {
  2256  		return res
  2257  	}
  2258  	pag := pager.NewThreadPager()
  2259  	res = new(chat1.Pagination)
  2260  	res.Num = control.Num
  2261  	if control.Pivot != nil {
  2262  		var err error
  2263  		pm := pagerMsg{msgID: *control.Pivot}
  2264  		switch control.Mode {
  2265  		case chat1.MessageIDControlMode_OLDERMESSAGES:
  2266  			res.Next, err = pag.MakeIndex(pm)
  2267  		case chat1.MessageIDControlMode_NEWERMESSAGES:
  2268  			res.Previous, err = pag.MakeIndex(pm)
  2269  		case chat1.MessageIDControlMode_UNREADLINE:
  2270  			if conv == nil {
  2271  				// just bail out of here with no conversation
  2272  				logger.Debug(ctx, "MessageIDControlToPagination: unreadline mode with no conv, bailing")
  2273  				return nil
  2274  			}
  2275  			pm.msgID = conv.Conv.ReaderInfo.ReadMsgid
  2276  			fallthrough
  2277  		case chat1.MessageIDControlMode_CENTERED:
  2278  			// Heuristic that we might want to revisit, get older messages from a little ahead of where
  2279  			// we want to center on
  2280  			if conv == nil {
  2281  				// just bail out of here with no conversation
  2282  				logger.Debug(ctx, "MessageIDControlToPagination: centered mode with no conv, bailing")
  2283  				return nil
  2284  			}
  2285  			maxID := int(conv.Conv.MaxVisibleMsgID())
  2286  			desired := int(pm.msgID) + control.Num/2
  2287  			logger.Debug(ctx, "MessageIDControlToPagination: maxID: %d desired: %d", maxID, desired)
  2288  			if desired > maxID {
  2289  				desired = maxID
  2290  			}
  2291  			pm.msgID = chat1.MessageID(desired + 1)
  2292  			res.Next, err = pag.MakeIndex(pm)
  2293  			res.ForceFirstPage = true
  2294  		}
  2295  		if err != nil {
  2296  			return nil
  2297  		}
  2298  	}
  2299  	return res
  2300  }
  2301  
  2302  // AssetsForMessage gathers all assets on a message
  2303  func AssetsForMessage(g *globals.Context, msgBody chat1.MessageBody) (assets []chat1.Asset) {
  2304  	typ, err := msgBody.MessageType()
  2305  	if err != nil {
  2306  		// Log and drop the error for a malformed MessageBody.
  2307  		g.Log.Warning("error getting assets for message: %s", err)
  2308  		return assets
  2309  	}
  2310  	switch typ {
  2311  	case chat1.MessageType_ATTACHMENT:
  2312  		body := msgBody.Attachment()
  2313  		if body.Object.Path != "" {
  2314  			assets = append(assets, body.Object)
  2315  		}
  2316  		if body.Preview != nil {
  2317  			assets = append(assets, *body.Preview)
  2318  		}
  2319  		assets = append(assets, body.Previews...)
  2320  	case chat1.MessageType_ATTACHMENTUPLOADED:
  2321  		body := msgBody.Attachmentuploaded()
  2322  		if body.Object.Path != "" {
  2323  			assets = append(assets, body.Object)
  2324  		}
  2325  		assets = append(assets, body.Previews...)
  2326  	}
  2327  	return assets
  2328  }
  2329  
  2330  func AddUserToTLFName(g *globals.Context, tlfName string, vis keybase1.TLFVisibility,
  2331  	membersType chat1.ConversationMembersType) string {
  2332  	switch membersType {
  2333  	case chat1.ConversationMembersType_IMPTEAMNATIVE, chat1.ConversationMembersType_IMPTEAMUPGRADE,
  2334  		chat1.ConversationMembersType_KBFS:
  2335  		if vis == keybase1.TLFVisibility_PUBLIC {
  2336  			return tlfName
  2337  		}
  2338  
  2339  		username := g.Env.GetUsername().String()
  2340  		if len(tlfName) == 0 {
  2341  			return username
  2342  		}
  2343  
  2344  		// KBFS creates TLFs with suffixes (e.g., folder names that
  2345  		// conflict after an assertion has been resolved) and readers,
  2346  		// so we need to handle those types of TLF names here so that
  2347  		// edit history works correctly.
  2348  		split1 := strings.SplitN(tlfName, " ", 2) // split off suffix
  2349  		split2 := strings.Split(split1[0], "#")   // split off readers
  2350  		// Add the name to the writers list (assume the current user
  2351  		// is a writer).
  2352  		tlfName = split2[0] + "," + username
  2353  		if len(split2) > 1 {
  2354  			// Re-append any readers.
  2355  			tlfName += "#" + split2[1]
  2356  		}
  2357  		if len(split1) > 1 {
  2358  			// Re-append any suffix.
  2359  			tlfName += " " + split1[1]
  2360  		}
  2361  		return tlfName
  2362  	default:
  2363  		return tlfName
  2364  	}
  2365  }
  2366  
  2367  func ForceReloadUPAKsForUIDs(ctx context.Context, g *globals.Context, uids []keybase1.UID) error {
  2368  	getArg := func(i int) *libkb.LoadUserArg {
  2369  		if i >= len(uids) {
  2370  			return nil
  2371  		}
  2372  		tmp := libkb.NewLoadUserByUIDForceArg(g.GlobalContext, uids[i])
  2373  		return &tmp
  2374  	}
  2375  	return g.GetUPAKLoader().Batcher(ctx, getArg, nil, 0)
  2376  }
  2377  
  2378  func CreateHiddenPlaceholder(msgID chat1.MessageID) chat1.MessageUnboxed {
  2379  	return chat1.NewMessageUnboxedWithPlaceholder(
  2380  		chat1.MessageUnboxedPlaceholder{
  2381  			MessageID: msgID,
  2382  			Hidden:    true,
  2383  		})
  2384  }
  2385  
  2386  func GetGregorConn(ctx context.Context, g *globals.Context, log DebugLabeler,
  2387  	handler func(nist *libkb.NIST) rpc.ConnectionHandler) (conn *rpc.Connection, token gregor1.SessionToken, err error) {
  2388  	// Get session token
  2389  	nist, _, _, err := g.ActiveDevice.NISTAndUIDDeviceID(ctx)
  2390  	if nist == nil {
  2391  		log.Debug(ctx, "GetGregorConn: got a nil NIST, is the user logged out?")
  2392  		return conn, token, libkb.LoggedInError{}
  2393  	}
  2394  	if err != nil {
  2395  		log.Debug(ctx, "GetGregorConn: failed to get logged in session: %s", err.Error())
  2396  		return conn, token, err
  2397  	}
  2398  	token = gregor1.SessionToken(nist.Token().String())
  2399  
  2400  	// Make an ad hoc connection to gregor
  2401  	uri, err := rpc.ParseFMPURI(g.Env.GetGregorURI())
  2402  	if err != nil {
  2403  		log.Debug(ctx, "GetGregorConn: failed to parse chat server UR: %s", err.Error())
  2404  		return conn, token, err
  2405  	}
  2406  
  2407  	if uri.UseTLS() {
  2408  		rawCA := g.Env.GetBundledCA(uri.Host)
  2409  		if len(rawCA) == 0 {
  2410  			err := errors.New("len(rawCA) == 0")
  2411  			log.Debug(ctx, "GetGregorConn: failed to parse CAs", err.Error())
  2412  			return conn, token, err
  2413  		}
  2414  		conn = rpc.NewTLSConnectionWithDialable(rpc.NewFixedRemote(uri.HostPort),
  2415  			[]byte(rawCA), libkb.NewContextifiedErrorUnwrapper(g.ExternalG()),
  2416  			handler(nist), libkb.NewRPCLogFactory(g.ExternalG()),
  2417  			g.ExternalG().RemoteNetworkInstrumenterStorage,
  2418  			logger.LogOutputWithDepthAdder{Logger: g.Log},
  2419  			rpc.DefaultMaxFrameLength, rpc.ConnectionOpts{},
  2420  			libkb.NewProxyDialable(g.Env))
  2421  	} else {
  2422  		t := rpc.NewConnectionTransportWithDialable(uri, nil,
  2423  			g.ExternalG().RemoteNetworkInstrumenterStorage,
  2424  			libkb.MakeWrapError(g.ExternalG()),
  2425  			rpc.DefaultMaxFrameLength, libkb.NewProxyDialable(g.GetEnv()))
  2426  		conn = rpc.NewConnectionWithTransport(handler(nist), t,
  2427  			libkb.NewContextifiedErrorUnwrapper(g.ExternalG()),
  2428  			logger.LogOutputWithDepthAdder{Logger: g.Log}, rpc.ConnectionOpts{})
  2429  	}
  2430  	return conn, token, nil
  2431  }
  2432  
  2433  // GetQueryRe returns a regex to match the query string on message text. This
  2434  // is used for result highlighting.
  2435  func GetQueryRe(query string) (*regexp.Regexp, error) {
  2436  	return regexp.Compile("(?i)" + regexp.QuoteMeta(query))
  2437  }
  2438  
  2439  func SetUnfurl(mvalid *chat1.MessageUnboxedValid, unfurlMessageID chat1.MessageID,
  2440  	unfurl chat1.UnfurlResult) {
  2441  	if mvalid.Unfurls == nil {
  2442  		mvalid.Unfurls = make(map[chat1.MessageID]chat1.UnfurlResult)
  2443  	}
  2444  	mvalid.Unfurls[unfurlMessageID] = unfurl
  2445  }
  2446  
  2447  func RemoveUnfurl(mvalid *chat1.MessageUnboxedValid, unfurlMessageID chat1.MessageID) {
  2448  	if mvalid.Unfurls == nil {
  2449  		return
  2450  	}
  2451  	delete(mvalid.Unfurls, unfurlMessageID)
  2452  }
  2453  
  2454  // SuspendComponent will suspend a Suspendable type until the return function
  2455  // is called. This allows a succinct call like defer SuspendComponent(ctx, g,
  2456  // g.ConvLoader)() in RPC handlers wishing to lock out the conv loader.
  2457  func SuspendComponent(ctx context.Context, g *globals.Context, suspendable types.Suspendable) func() {
  2458  	if canceled := suspendable.Suspend(ctx); canceled {
  2459  		g.Log.CDebugf(ctx, "SuspendComponent: canceled background task")
  2460  	}
  2461  	return func() {
  2462  		suspendable.Resume(ctx)
  2463  	}
  2464  }
  2465  
  2466  func SuspendComponents(ctx context.Context, g *globals.Context, suspendables []types.Suspendable) func() {
  2467  	resumeFuncs := make([]func(), 0, len(suspendables))
  2468  	for _, s := range suspendables {
  2469  		resumeFuncs = append(resumeFuncs, SuspendComponent(ctx, g, s))
  2470  	}
  2471  	return func() {
  2472  		for _, f := range resumeFuncs {
  2473  			f()
  2474  		}
  2475  	}
  2476  }
  2477  
  2478  func IsPermanentErr(err error) bool {
  2479  	if uberr, ok := err.(types.UnboxingError); ok {
  2480  		return uberr.IsPermanent()
  2481  	}
  2482  	return err != nil
  2483  }
  2484  
  2485  func EphemeralLifetimeFromConv(ctx context.Context, g *globals.Context, conv chat1.ConversationLocal) (res *gregor1.DurationSec, err error) {
  2486  	// Check to see if the conversation has an exploding policy
  2487  	var retentionRes *gregor1.DurationSec
  2488  	var gregorRes *gregor1.DurationSec
  2489  	var rentTyp chat1.RetentionPolicyType
  2490  	var convSet bool
  2491  	if conv.ConvRetention != nil {
  2492  		if rentTyp, err = conv.ConvRetention.Typ(); err != nil {
  2493  			return res, err
  2494  		}
  2495  		if rentTyp == chat1.RetentionPolicyType_EPHEMERAL {
  2496  			e := conv.ConvRetention.Ephemeral()
  2497  			retentionRes = &e.Age
  2498  		}
  2499  		convSet = rentTyp != chat1.RetentionPolicyType_INHERIT
  2500  	}
  2501  	if !convSet && conv.TeamRetention != nil {
  2502  		if rentTyp, err = conv.TeamRetention.Typ(); err != nil {
  2503  			return res, err
  2504  		}
  2505  		if rentTyp == chat1.RetentionPolicyType_EPHEMERAL {
  2506  			e := conv.TeamRetention.Ephemeral()
  2507  			retentionRes = &e.Age
  2508  		}
  2509  	}
  2510  
  2511  	// See if there is anything in Gregor
  2512  	st, err := g.GregorState.State(ctx)
  2513  	if err != nil {
  2514  		return res, err
  2515  	}
  2516  	// Note: this value is present on the JS frontend as well
  2517  	key := fmt.Sprintf("exploding:%s", conv.GetConvID())
  2518  	cat, err := gregor1.ObjFactory{}.MakeCategory(key)
  2519  	if err != nil {
  2520  		return res, err
  2521  	}
  2522  	items, err := st.ItemsWithCategoryPrefix(cat)
  2523  	if err != nil {
  2524  		return res, err
  2525  	}
  2526  	if len(items) > 0 {
  2527  		it := items[0]
  2528  		body := string(it.Body().Bytes())
  2529  		sec, err := strconv.ParseInt(body, 0, 0)
  2530  		if err != nil {
  2531  			return res, nil
  2532  		}
  2533  		gsec := gregor1.DurationSec(sec)
  2534  		gregorRes = &gsec
  2535  	}
  2536  	if retentionRes != nil && gregorRes != nil {
  2537  		if *gregorRes < *retentionRes {
  2538  			return gregorRes, nil
  2539  		}
  2540  		return retentionRes, nil
  2541  	} else if retentionRes != nil {
  2542  		return retentionRes, nil
  2543  	} else if gregorRes != nil {
  2544  		return gregorRes, nil
  2545  	} else {
  2546  		return nil, nil
  2547  	}
  2548  }
  2549  
  2550  var decorateBegin = "$>kb$"
  2551  var decorateEnd = "$<kb$"
  2552  var decorateEscapeRe = regexp.MustCompile(`\\*\$\>kb\$`)
  2553  
  2554  func EscapeForDecorate(ctx context.Context, body string) string {
  2555  	// escape any natural occurrences of begin so we don't bust markdown parser
  2556  	return decorateEscapeRe.ReplaceAllStringFunc(body, func(s string) string {
  2557  		if len(s)%2 != 0 {
  2558  			return `\` + s
  2559  		}
  2560  		return s
  2561  	})
  2562  }
  2563  
  2564  func DecorateBody(ctx context.Context, body string, offset, length int, decoration interface{}) (res string, added int) {
  2565  	out, err := json.Marshal(decoration)
  2566  	if err != nil {
  2567  		return res, 0
  2568  	}
  2569  	b64out := base64.StdEncoding.EncodeToString(out)
  2570  	strDecoration := fmt.Sprintf("%s%s%s", decorateBegin, b64out, decorateEnd)
  2571  	added = len(strDecoration) - length
  2572  	res = fmt.Sprintf("%s%s%s", body[:offset], strDecoration, body[offset+length:])
  2573  	return res, added
  2574  }
  2575  
  2576  var linkRegexp = xurls.Relaxed()
  2577  
  2578  // These indices correspond to the named capture groups in the xurls regexes
  2579  var linkRelaxedGroupIndex = 0
  2580  var linkStrictGroupIndex = 0
  2581  var mailtoRegexp = regexp.MustCompile(`(?:(?:[\w-_.]+)@(?:[\w-]+(?:\.[\w-]+)+))\b`)
  2582  
  2583  func init() {
  2584  	for index, name := range linkRegexp.SubexpNames() {
  2585  		if name == "relaxed" {
  2586  			linkRelaxedGroupIndex = index + 1
  2587  		}
  2588  		if name == "strict" {
  2589  			linkStrictGroupIndex = index + 1
  2590  		}
  2591  	}
  2592  }
  2593  
  2594  func DecorateWithLinks(ctx context.Context, body string) string {
  2595  	var added int
  2596  	offset := 0
  2597  	origBody := body
  2598  
  2599  	// early out of here if there is no dot
  2600  	if !(strings.Contains(body, ".") || strings.Contains(body, "://")) {
  2601  		return body
  2602  	}
  2603  	shouldSkipLink := func(linkPrefix, link string) bool {
  2604  		// Check for RTLO character preceeding our match. If one is
  2605  		// detected, and there isn't a LTRO following it, skip this URL
  2606  		// from linkification.
  2607  		if rtloIdx := strings.LastIndex(linkPrefix, "\u202e"); rtloIdx >= 0 && rtloIdx > strings.LastIndex(linkPrefix, "\u202d") {
  2608  			return true
  2609  		}
  2610  		if strings.Contains(strings.Split(link, "/")[0], "@") {
  2611  			return true
  2612  		}
  2613  		for _, scheme := range xurls.SchemesNoAuthority {
  2614  			if strings.HasPrefix(link, scheme) {
  2615  				return true
  2616  			}
  2617  		}
  2618  		if strings.HasPrefix(link, "ftp://") || strings.HasPrefix(link, "gopher://") {
  2619  			return true
  2620  		}
  2621  		return false
  2622  	}
  2623  	allMatches := linkRegexp.FindAllStringSubmatchIndex(ReplaceQuotedSubstrings(body, true), -1)
  2624  	for _, match := range allMatches {
  2625  		var lowhit, highhit int
  2626  		if len(match) >= linkRelaxedGroupIndex*2 && match[linkRelaxedGroupIndex*2-2] >= 0 {
  2627  			lowhit = linkRelaxedGroupIndex*2 - 2
  2628  			highhit = linkRelaxedGroupIndex*2 - 1
  2629  		} else if len(match) >= linkStrictGroupIndex*2 && match[linkStrictGroupIndex*2-2] >= 0 {
  2630  			lowhit = linkStrictGroupIndex*2 - 2
  2631  			highhit = linkStrictGroupIndex*2 - 1
  2632  		} else {
  2633  			continue
  2634  		}
  2635  
  2636  		bodyPrefix := origBody[:match[lowhit]]
  2637  		bodyMatch := origBody[match[lowhit]:match[highhit]]
  2638  		if shouldSkipLink(bodyPrefix, bodyMatch) {
  2639  			continue
  2640  		}
  2641  		var punycode string
  2642  		url := bodyMatch
  2643  		if encoded, err := idna.ToASCII(url); err == nil && encoded != url {
  2644  			punycode = encoded
  2645  		}
  2646  		body, added = DecorateBody(ctx, body, match[lowhit]+offset, match[highhit]-match[lowhit],
  2647  			chat1.NewUITextDecorationWithLink(chat1.UILinkDecoration{
  2648  				Url:      bodyMatch,
  2649  				Punycode: punycode,
  2650  			}))
  2651  		offset += added
  2652  	}
  2653  
  2654  	offset = 0
  2655  	origBody = body
  2656  	allMatches = mailtoRegexp.FindAllStringIndex(ReplaceQuotedSubstrings(body, true), -1)
  2657  	for _, match := range allMatches {
  2658  		if len(match) < 2 {
  2659  			continue
  2660  		}
  2661  		bodyMatch := origBody[match[0]:match[1]]
  2662  		body, added = DecorateBody(ctx, body, match[0]+offset, match[1]-match[0],
  2663  			chat1.NewUITextDecorationWithMailto(chat1.UILinkDecoration{
  2664  				Url: bodyMatch,
  2665  			}))
  2666  		offset += added
  2667  	}
  2668  
  2669  	return body
  2670  }
  2671  
  2672  func DecorateWithMentions(ctx context.Context, body string, atMentions []string,
  2673  	maybeMentions []chat1.MaybeMention, chanMention chat1.ChannelMention,
  2674  	channelNameMentions []chat1.ChannelNameMention) string {
  2675  	var added int
  2676  	offset := 0
  2677  	if len(atMentions) > 0 || len(maybeMentions) > 0 || chanMention != chat1.ChannelMention_NONE {
  2678  		atMap := make(map[string]bool)
  2679  		for _, at := range atMentions {
  2680  			atMap[at] = true
  2681  		}
  2682  		maybeMap := make(map[string]chat1.MaybeMention)
  2683  		for _, tm := range maybeMentions {
  2684  			name := tm.Name
  2685  			if len(tm.Channel) > 0 {
  2686  				name += "#" + tm.Channel
  2687  			}
  2688  			maybeMap[name] = tm
  2689  		}
  2690  		inputBody := body
  2691  		atMatches := parseRegexpNames(ctx, inputBody, atMentionRegExp)
  2692  		for _, m := range atMatches {
  2693  			switch {
  2694  			case m.normalizedName == "here":
  2695  				fallthrough
  2696  			case m.normalizedName == "channel":
  2697  				fallthrough
  2698  			case m.normalizedName == "everyone":
  2699  				if chanMention == chat1.ChannelMention_NONE {
  2700  					continue
  2701  				}
  2702  				fallthrough
  2703  			case atMap[m.normalizedName]:
  2704  				body, added = DecorateBody(ctx, body, m.position[0]+offset-1, m.Len()+1,
  2705  					chat1.NewUITextDecorationWithAtmention(m.name))
  2706  				offset += added
  2707  			}
  2708  			if tm, ok := maybeMap[m.name]; ok {
  2709  				body, added = DecorateBody(ctx, body, m.position[0]+offset-1, m.Len()+1,
  2710  					chat1.NewUITextDecorationWithMaybemention(tm))
  2711  				offset += added
  2712  			}
  2713  		}
  2714  	}
  2715  	if len(channelNameMentions) > 0 {
  2716  		chanMap := make(map[string]chat1.ConversationID)
  2717  		for _, c := range channelNameMentions {
  2718  			chanMap[c.TopicName] = c.ConvID
  2719  		}
  2720  		offset = 0
  2721  		inputBody := body
  2722  		chanMatches := parseRegexpNames(ctx, inputBody, chanNameMentionRegExp)
  2723  		for _, c := range chanMatches {
  2724  			convID, ok := chanMap[c.name]
  2725  			if !ok {
  2726  				continue
  2727  			}
  2728  			body, added = DecorateBody(ctx, body, c.position[0]+offset-1, c.Len()+1,
  2729  				chat1.NewUITextDecorationWithChannelnamemention(chat1.UIChannelNameMention{
  2730  					Name:   c.name,
  2731  					ConvID: convID.ConvIDStr(),
  2732  				}))
  2733  			offset += added
  2734  		}
  2735  	}
  2736  	return body
  2737  }
  2738  
  2739  func EscapeShrugs(ctx context.Context, body string) string {
  2740  	return strings.ReplaceAll(body, `¯\_(ツ)_/¯`, `¯\\\_(ツ)_/¯`)
  2741  }
  2742  
  2743  var startQuote = ">"
  2744  var newline = []rune("\n")
  2745  
  2746  var blockQuoteRegex = regexp.MustCompile("((?s)```.*?```)")
  2747  var quoteRegex = regexp.MustCompile("((?s)`.*?`)")
  2748  
  2749  func ReplaceQuotedSubstrings(xs string, skipAngleQuotes bool) string {
  2750  	replacer := func(s string) string {
  2751  		return strings.Repeat("$", len(s))
  2752  	}
  2753  	xs = blockQuoteRegex.ReplaceAllStringFunc(xs, replacer)
  2754  	xs = quoteRegex.ReplaceAllStringFunc(xs, replacer)
  2755  
  2756  	// Remove all quoted lines. Because we removed all codeblocks
  2757  	// before, we only need to consider single lines.
  2758  	var ret []string
  2759  	for _, line := range strings.Split(xs, string(newline)) {
  2760  		if skipAngleQuotes || !strings.HasPrefix(strings.TrimLeft(line, " "), startQuote) {
  2761  			ret = append(ret, line)
  2762  		} else {
  2763  			ret = append(ret, replacer(line))
  2764  		}
  2765  	}
  2766  	return strings.Join(ret, string(newline))
  2767  }
  2768  
  2769  var ErrGetUnverifiedConvNotFound = errors.New("GetUnverifiedConv: conversation not found")
  2770  var ErrGetVerifiedConvNotFound = errors.New("GetVerifiedConv: conversation not found")
  2771  
  2772  func GetUnverifiedConv(ctx context.Context, g *globals.Context, uid gregor1.UID,
  2773  	convID chat1.ConversationID, dataSource types.InboxSourceDataSourceTyp) (res types.RemoteConversation, err error) {
  2774  
  2775  	inbox, err := g.InboxSource.ReadUnverified(ctx, uid, dataSource, &chat1.GetInboxQuery{
  2776  		ConvIDs:      []chat1.ConversationID{convID},
  2777  		MemberStatus: chat1.AllConversationMemberStatuses(),
  2778  	})
  2779  	if err != nil {
  2780  		return res, err
  2781  	}
  2782  	if len(inbox.ConvsUnverified) == 0 {
  2783  		return res, ErrGetUnverifiedConvNotFound
  2784  	}
  2785  	if !inbox.ConvsUnverified[0].GetConvID().Eq(convID) {
  2786  		return res, fmt.Errorf("GetUnverifiedConv: convID mismatch: %s != %s",
  2787  			inbox.ConvsUnverified[0].ConvIDStr, convID)
  2788  	}
  2789  	return inbox.ConvsUnverified[0], nil
  2790  }
  2791  
  2792  func FormatConversationName(info chat1.ConversationInfoLocal, myUsername string) string {
  2793  	switch info.TeamType {
  2794  	case chat1.TeamType_COMPLEX:
  2795  		if len(info.TlfName) > 0 && len(info.TopicName) > 0 {
  2796  			return fmt.Sprintf("%s#%s", info.TlfName, info.TopicName)
  2797  		}
  2798  		return info.TlfName
  2799  	case chat1.TeamType_SIMPLE:
  2800  		return info.TlfName
  2801  	case chat1.TeamType_NONE:
  2802  		users := info.Participants
  2803  		if len(users) == 1 {
  2804  			return ""
  2805  		}
  2806  		var usersWithoutYou []string
  2807  		for _, user := range users {
  2808  			if user.Username != myUsername && user.InConvName {
  2809  				usersWithoutYou = append(usersWithoutYou, user.Username)
  2810  			}
  2811  		}
  2812  		return strings.Join(usersWithoutYou, ",")
  2813  	default:
  2814  		return ""
  2815  	}
  2816  }
  2817  
  2818  func GetVerifiedConv(ctx context.Context, g *globals.Context, uid gregor1.UID,
  2819  	convID chat1.ConversationID, dataSource types.InboxSourceDataSourceTyp) (res chat1.ConversationLocal, err error) {
  2820  	// in case we are being called from within some cancelable context, remove
  2821  	// it for the purposes of this call, since whatever this is is likely a
  2822  	// side effect we don't want to get stuck
  2823  	ctx = globals.CtxRemoveLocalizerCancelable(ctx)
  2824  	inbox, _, err := g.InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking, dataSource, nil,
  2825  		&chat1.GetInboxLocalQuery{
  2826  			ConvIDs:      []chat1.ConversationID{convID},
  2827  			MemberStatus: chat1.AllConversationMemberStatuses(),
  2828  		})
  2829  	if err != nil {
  2830  		return res, err
  2831  	}
  2832  	if len(inbox.Convs) == 0 {
  2833  		return res, ErrGetVerifiedConvNotFound
  2834  	}
  2835  	if !inbox.Convs[0].GetConvID().Eq(convID) {
  2836  		return res, fmt.Errorf("GetVerifiedConv: convID mismatch: %s != %s",
  2837  			inbox.Convs[0].GetConvID(), convID)
  2838  	}
  2839  	return inbox.Convs[0], nil
  2840  }
  2841  
  2842  func IsMapUnfurl(msg chat1.MessageUnboxed) bool {
  2843  	if !msg.IsValid() {
  2844  		return false
  2845  	}
  2846  	body := msg.Valid().MessageBody
  2847  	if !body.IsType(chat1.MessageType_UNFURL) {
  2848  		return false
  2849  	}
  2850  	unfurl := body.Unfurl()
  2851  	typ, err := unfurl.Unfurl.Unfurl.UnfurlType()
  2852  	if err != nil {
  2853  		return false
  2854  	}
  2855  	if typ != chat1.UnfurlType_GENERIC {
  2856  		return false
  2857  	}
  2858  	return body.Unfurl().Unfurl.Unfurl.Generic().MapInfo != nil
  2859  }
  2860  
  2861  func DedupStringLists(lists ...[]string) (res []string) {
  2862  	seen := make(map[string]struct{})
  2863  	for _, list := range lists {
  2864  		for _, x := range list {
  2865  			if _, ok := seen[x]; !ok {
  2866  				seen[x] = struct{}{}
  2867  				res = append(res, x)
  2868  			}
  2869  		}
  2870  	}
  2871  	return res
  2872  }
  2873  
  2874  func DBConvLess(a pager.InboxEntry, b pager.InboxEntry) bool {
  2875  	if a.GetMtime() > b.GetMtime() {
  2876  		return true
  2877  	} else if a.GetMtime() < b.GetMtime() {
  2878  		return false
  2879  	}
  2880  	return !(a.GetConvID().Eq(b.GetConvID()) || a.GetConvID().Less(b.GetConvID()))
  2881  }
  2882  
  2883  func ExportToSummary(i chat1.InboxUIItem) (s chat1.ConvSummary) {
  2884  	s.Id = i.ConvID
  2885  	s.IsDefaultConv = i.IsDefaultConv
  2886  	s.Unread = i.ReadMsgID < i.MaxVisibleMsgID
  2887  	s.ActiveAt = i.Time.UnixSeconds()
  2888  	s.ActiveAtMs = i.Time.UnixMilliseconds()
  2889  	s.FinalizeInfo = i.FinalizeInfo
  2890  	s.CreatorInfo = i.CreatorInfo
  2891  	s.MemberStatus = strings.ToLower(i.MemberStatus.String())
  2892  	s.Supersedes = make([]string, 0, len(i.Supersedes))
  2893  	for _, super := range i.Supersedes {
  2894  		s.Supersedes = append(s.Supersedes,
  2895  			super.ConversationID.String())
  2896  	}
  2897  	s.SupersededBy = make([]string, 0, len(i.SupersededBy))
  2898  	for _, super := range i.SupersededBy {
  2899  		s.SupersededBy = append(s.SupersededBy,
  2900  			super.ConversationID.String())
  2901  	}
  2902  	switch i.MembersType {
  2903  	case chat1.ConversationMembersType_IMPTEAMUPGRADE, chat1.ConversationMembersType_IMPTEAMNATIVE:
  2904  		s.ResetUsers = i.ResetParticipants
  2905  	}
  2906  	s.Channel = chat1.ChatChannel{
  2907  		Name:        i.Name,
  2908  		Public:      i.IsPublic,
  2909  		TopicType:   strings.ToLower(i.TopicType.String()),
  2910  		MembersType: strings.ToLower(i.MembersType.String()),
  2911  		TopicName:   i.Channel,
  2912  	}
  2913  	return s
  2914  }
  2915  
  2916  func supersedersNotEmpty(ctx context.Context, superseders []chat1.ConversationMetadata, convs []types.RemoteConversation) bool {
  2917  	for _, superseder := range superseders {
  2918  		for _, conv := range convs {
  2919  			if superseder.ConversationID.Eq(conv.GetConvID()) {
  2920  				for _, msg := range conv.Conv.MaxMsgSummaries {
  2921  					if IsNonEmptyConvMessageType(msg.GetMessageType()) {
  2922  						return true
  2923  					}
  2924  				}
  2925  			}
  2926  		}
  2927  	}
  2928  	return false
  2929  }
  2930  
  2931  var defaultMemberStatusFilter = []chat1.ConversationMemberStatus{
  2932  	chat1.ConversationMemberStatus_ACTIVE,
  2933  	chat1.ConversationMemberStatus_PREVIEW,
  2934  	chat1.ConversationMemberStatus_RESET,
  2935  }
  2936  
  2937  var defaultExistences = []chat1.ConversationExistence{
  2938  	chat1.ConversationExistence_ACTIVE,
  2939  }
  2940  
  2941  func ApplyInboxQuery(ctx context.Context, debugLabeler DebugLabeler, query *chat1.GetInboxQuery, rcs []types.RemoteConversation) (res []types.RemoteConversation) {
  2942  	if query == nil {
  2943  		query = &chat1.GetInboxQuery{}
  2944  	}
  2945  
  2946  	var queryConvIDMap map[chat1.ConvIDStr]bool
  2947  	if query.ConvID != nil {
  2948  		query.ConvIDs = append(query.ConvIDs, *query.ConvID)
  2949  	}
  2950  	if len(query.ConvIDs) > 0 {
  2951  		queryConvIDMap = make(map[chat1.ConvIDStr]bool, len(query.ConvIDs))
  2952  		for _, c := range query.ConvIDs {
  2953  			queryConvIDMap[c.ConvIDStr()] = true
  2954  		}
  2955  	}
  2956  
  2957  	memberStatus := query.MemberStatus
  2958  	if len(memberStatus) == 0 {
  2959  		memberStatus = defaultMemberStatusFilter
  2960  	}
  2961  	queryMemberStatusMap := map[chat1.ConversationMemberStatus]bool{}
  2962  	for _, memberStatus := range memberStatus {
  2963  		queryMemberStatusMap[memberStatus] = true
  2964  	}
  2965  
  2966  	queryStatusMap := map[chat1.ConversationStatus]bool{}
  2967  	for _, status := range query.Status {
  2968  		queryStatusMap[status] = true
  2969  	}
  2970  
  2971  	existences := query.Existences
  2972  	if len(existences) == 0 {
  2973  		existences = defaultExistences
  2974  	}
  2975  	existenceMap := map[chat1.ConversationExistence]bool{}
  2976  	for _, status := range existences {
  2977  		existenceMap[status] = true
  2978  	}
  2979  
  2980  	for _, rc := range rcs {
  2981  		conv := rc.Conv
  2982  		// Existence check
  2983  		if _, ok := existenceMap[conv.Metadata.Existence]; !ok && len(existenceMap) > 0 {
  2984  			continue
  2985  		}
  2986  		// Member status check
  2987  		if _, ok := queryMemberStatusMap[conv.ReaderInfo.Status]; !ok && len(memberStatus) > 0 {
  2988  			continue
  2989  		}
  2990  		// Status check
  2991  		if _, ok := queryStatusMap[conv.Metadata.Status]; !ok && len(query.Status) > 0 {
  2992  			continue
  2993  		}
  2994  		// Basic checks
  2995  		if queryConvIDMap != nil && !queryConvIDMap[rc.ConvIDStr] {
  2996  			continue
  2997  		}
  2998  		if query.After != nil && !conv.ReaderInfo.Mtime.After(*query.After) {
  2999  			continue
  3000  		}
  3001  		if query.Before != nil && !conv.ReaderInfo.Mtime.Before(*query.Before) {
  3002  			continue
  3003  		}
  3004  		if query.TopicType != nil && *query.TopicType != conv.Metadata.IdTriple.TopicType {
  3005  			continue
  3006  		}
  3007  		if query.TlfVisibility != nil && *query.TlfVisibility != keybase1.TLFVisibility_ANY &&
  3008  			*query.TlfVisibility != conv.Metadata.Visibility {
  3009  			continue
  3010  		}
  3011  		if query.UnreadOnly && !conv.IsUnread() {
  3012  			continue
  3013  		}
  3014  		if query.ReadOnly && conv.IsUnread() {
  3015  			continue
  3016  		}
  3017  		if query.TlfID != nil && !query.TlfID.Eq(conv.Metadata.IdTriple.Tlfid) {
  3018  			continue
  3019  		}
  3020  		if query.TopicName != nil && rc.LocalMetadata != nil &&
  3021  			*query.TopicName != rc.LocalMetadata.TopicName {
  3022  			continue
  3023  		}
  3024  		// If we are finalized and are superseded, then don't return this
  3025  		if query.OneChatTypePerTLF == nil ||
  3026  			(query.OneChatTypePerTLF != nil && *query.OneChatTypePerTLF) {
  3027  			if conv.Metadata.FinalizeInfo != nil && len(conv.Metadata.SupersededBy) > 0 && len(query.ConvIDs) == 0 {
  3028  				if supersedersNotEmpty(ctx, conv.Metadata.SupersededBy, rcs) {
  3029  					continue
  3030  				}
  3031  			}
  3032  		}
  3033  		res = append(res, rc)
  3034  	}
  3035  	filtered := len(rcs) - len(res)
  3036  	debugLabeler.Debug(ctx, "applyQuery: query: %+v, res size: %d filtered: %d", query, len(res), filtered)
  3037  	return res
  3038  }
  3039  
  3040  func ToLastActiveStatus(mtime gregor1.Time) chat1.LastActiveStatus {
  3041  	lastActive := int(time.Since(mtime.Time()).Round(time.Hour).Hours())
  3042  	switch {
  3043  	case lastActive <= 24: // 1 day
  3044  		return chat1.LastActiveStatus_ACTIVE
  3045  	case lastActive <= 24*7: // 7 days
  3046  		return chat1.LastActiveStatus_RECENTLY_ACTIVE
  3047  	default:
  3048  		return chat1.LastActiveStatus_NONE
  3049  	}
  3050  }
  3051  
  3052  func GetConvParticipantUsernames(ctx context.Context, g *globals.Context, uid gregor1.UID,
  3053  	convID chat1.ConversationID) (parts []string, err error) {
  3054  	uids, err := g.ParticipantsSource.Get(ctx, uid, convID, types.InboxSourceDataSourceAll)
  3055  	if err != nil {
  3056  		return parts, err
  3057  	}
  3058  	kuids := make([]keybase1.UID, 0, len(uids))
  3059  	for _, uid := range uids {
  3060  		kuids = append(kuids, keybase1.UID(uid.String()))
  3061  	}
  3062  	rows, err := g.UIDMapper.MapUIDsToUsernamePackages(ctx, g, kuids, 0, 0, false)
  3063  	if err != nil {
  3064  		return parts, err
  3065  	}
  3066  	parts = make([]string, 0, len(rows))
  3067  	for _, row := range rows {
  3068  		parts = append(parts, row.NormalizedUsername.String())
  3069  	}
  3070  	return parts, nil
  3071  }
  3072  
  3073  func IsDeletedConvError(err error) bool {
  3074  	switch err.(type) {
  3075  	case libkb.ChatBadConversationError,
  3076  		libkb.ChatNotInTeamError,
  3077  		libkb.ChatNotInConvError:
  3078  		return true
  3079  	default:
  3080  		return false
  3081  	}
  3082  }