1package utils
2
3import (
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	"github.com/keybase/xurls"
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
37func 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".
51func 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
75func 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
94func 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
106func 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
120func 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
132func 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.
144func 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.
212func 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.
223func 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
252func isPhoneOrEmail(username string) bool {
253	return strings.HasSuffix(username, "@phone") || strings.HasSuffix(username, "@email")
254}
255
256const (
257	ChatTopicIDLen    = 16
258	ChatTopicIDSuffix = 0x20
259)
260
261func 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
269func 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
279type 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
295type 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
306func 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
355func 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
416type byConversationStatus []chat1.ConversationStatus
417
418func (c byConversationStatus) Len() int           { return len(c) }
419func (c byConversationStatus) Less(i, j int) bool { return c[i] < c[j] }
420func (c byConversationStatus) Swap(i, j int)      { c[i], c[j] = c[j], c[i] }
421
422// Which convs show in the inbox.
423func 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
434func 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
443func IsVisibleChatMessageType(messageType chat1.MessageType) bool {
444	return checkMessageTypeQual(messageType, chat1.VisibleChatMessageTypes())
445}
446
447func IsSnippetChatMessageType(messageType chat1.MessageType) bool {
448	return checkMessageTypeQual(messageType, chat1.SnippetChatMessageTypes())
449}
450
451func IsBadgeableMessageType(messageType chat1.MessageType) bool {
452	return checkMessageTypeQual(messageType, chat1.BadgeableMessageTypes())
453}
454
455func IsNonEmptyConvMessageType(messageType chat1.MessageType) bool {
456	return checkMessageTypeQual(messageType, chat1.NonEmptyConvMessageTypes())
457}
458
459func IsEditableByEditMessageType(messageType chat1.MessageType) bool {
460	return checkMessageTypeQual(messageType, chat1.EditableMessageTypesByEdit())
461}
462
463func 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
478func 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
486func 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
503type DebugLabeler struct {
504	libkb.Contextified
505	label   string
506	verbose bool
507}
508
509func 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
517func (d DebugLabeler) GetLog() logger.Logger {
518	return d.G().GetLog()
519}
520
521func (d DebugLabeler) GetPerfLog() logger.Logger {
522	return d.G().GetPerfLog()
523}
524
525func (d DebugLabeler) showVerbose() bool {
526	return false
527}
528
529func (d DebugLabeler) showLog() bool {
530	if d.verbose {
531		return d.showVerbose()
532	}
533	return true
534}
535
536func (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
542func (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
546func (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
550func (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.
566func 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)
604func 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
615func 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
631func 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
661const ServiceDecorationPrefix = `(?:^|[\s([/{:;.,!?"'])`
662
663var chanNameMentionRegExp = regexp.MustCompile(ServiceDecorationPrefix + `(#(?:[0-9a-zA-Z_-]+))`)
664
665func 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
687var atMentionRegExp = regexp.MustCompile(ServiceDecorationPrefix +
688	`(@(?:[a-zA-Z0-9][a-zA-Z0-9._]*[a-zA-Z0-9_]+(?:#[a-z0-9A-Z_-]+)?))`)
689
690type nameMatch struct {
691	name, normalizedName string
692	position             []int
693}
694
695func (m nameMatch) Len() int {
696	return m.position[1] - m.position[0]
697}
698
699func 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
718func 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
733func 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
749func 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
785func 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
827func 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
873func 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
881func 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
889func 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
897func 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
911func 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
919func 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
927func 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
935func SanitizeTopicName(topicName string) string {
936	return strings.TrimPrefix(topicName, "#")
937}
938
939func 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
956func 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
967func 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.
991func 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
999type MessageSummaryContainer interface {
1000	GetMaxMessage(typ chat1.MessageType) (chat1.MessageSummary, error)
1001}
1002
1003func 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
1022func 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
1030func 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
1041func 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
1054func 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
1065func 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
1074func 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
1086func 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
1097func 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
1109func 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
1143func 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
1211func 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
1256func 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
1307func StripUsernameFromConvName(name string, username string) (res string) {
1308	res = strings.Replace(name, fmt.Sprintf(",%s", username), "", -1)
1309	res = strings.Replace(res, fmt.Sprintf("%s,", username), "", -1)
1310	return res
1311}
1312
1313func 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
1328func 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
1337func 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
1389func 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
1397func SearchableRemoteConversationName(conv types.RemoteConversation, username string) string {
1398	name := GetRemoteConvDisplayName(conv)
1399	// Check for self conv or big team conv
1400	if name == username || strings.Contains(name, "#") {
1401		return name
1402	}
1403	name = strings.Replace(name, fmt.Sprintf(",%s", username), "", -1)
1404	name = strings.Replace(name, fmt.Sprintf("%s,", username), "", -1)
1405	return name
1406}
1407
1408func PresentRemoteConversationAsSearchHit(conv types.RemoteConversation, username string) chat1.UIChatSearchConvHit {
1409	return chat1.UIChatSearchConvHit{
1410		ConvID:   conv.ConvIDStr,
1411		TeamType: conv.GetTeamType(),
1412		Name:     SearchableRemoteConversationName(conv, username),
1413		Mtime:    conv.GetMtime(),
1414	}
1415}
1416
1417func PresentRemoteConversationsAsSearchHits(convs []types.RemoteConversation, username string) (res []chat1.UIChatSearchConvHit) {
1418	res = make([]chat1.UIChatSearchConvHit, 0, len(convs))
1419	for _, c := range convs {
1420		res = append(res, PresentRemoteConversationAsSearchHit(c, username))
1421	}
1422	return res
1423}
1424
1425func PresentConversationErrorLocal(ctx context.Context, g *globals.Context, uid gregor1.UID, rawConv chat1.ConversationErrorLocal) (res chat1.InboxUIItemError) {
1426	res.Message = rawConv.Message
1427	res.RekeyInfo = rawConv.RekeyInfo
1428	res.RemoteConv = PresentRemoteConversation(ctx, g, uid, types.RemoteConversation{
1429		Conv:      rawConv.RemoteConv,
1430		ConvIDStr: rawConv.RemoteConv.GetConvID().ConvIDStr(),
1431	})
1432	res.Typ = rawConv.Typ
1433	res.UnverifiedTLFName = rawConv.UnverifiedTLFName
1434	return res
1435}
1436
1437func getParticipantType(username string) chat1.UIParticipantType {
1438	if strings.HasSuffix(username, "@phone") {
1439		return chat1.UIParticipantType_PHONENO
1440	}
1441	if strings.HasSuffix(username, "@email") {
1442		return chat1.UIParticipantType_EMAIL
1443	}
1444	return chat1.UIParticipantType_USER
1445}
1446
1447func PresentConversationParticipantsLocal(ctx context.Context, rawParticipants []chat1.ConversationLocalParticipant) (participants []chat1.UIParticipant) {
1448	participants = make([]chat1.UIParticipant, 0, len(rawParticipants))
1449	for _, p := range rawParticipants {
1450		participantType := getParticipantType(p.Username)
1451		participants = append(participants, chat1.UIParticipant{
1452			Assertion:   p.Username,
1453			InConvName:  p.InConvName,
1454			ContactName: p.ContactName,
1455			FullName:    p.Fullname,
1456			Type:        participantType,
1457		})
1458	}
1459	return participants
1460}
1461
1462type PresentParticipantsMode int
1463
1464const (
1465	PresentParticipantsModeInclude PresentParticipantsMode = iota
1466	PresentParticipantsModeSkip
1467)
1468
1469func PresentConversationLocal(ctx context.Context, g *globals.Context, uid gregor1.UID,
1470	rawConv chat1.ConversationLocal, partMode PresentParticipantsMode) (res chat1.InboxUIItem) {
1471	res.ConvID = rawConv.GetConvID().ConvIDStr()
1472	res.TlfID = rawConv.Info.Triple.Tlfid.TLFIDStr()
1473	res.TopicType = rawConv.GetTopicType()
1474	res.IsPublic = rawConv.Info.Visibility == keybase1.TLFVisibility_PUBLIC
1475	res.IsDefaultConv = rawConv.Info.IsDefaultConv
1476	res.Name = rawConv.Info.TlfName
1477	res.SnippetDecoration, res.Snippet, res.SnippetDecorated =
1478		GetConvSnippet(ctx, g, uid, rawConv, g.GetEnv().GetUsername().String())
1479	res.Channel = rawConv.Info.TopicName
1480	res.Headline = rawConv.Info.Headline
1481	res.HeadlineDecorated = DecorateWithLinks(ctx, PresentDecoratedSnippet(ctx, g, rawConv.Info.Headline, uid,
1482		chat1.MessageType_HEADLINE, rawConv.Info.HeadlineEmojis))
1483	res.ResetParticipants = rawConv.Info.ResetNames
1484	res.Status = rawConv.Info.Status
1485	res.MembersType = rawConv.GetMembersType()
1486	res.MemberStatus = rawConv.Info.MemberStatus
1487	res.Visibility = rawConv.Info.Visibility
1488	res.Time = GetConvMtimeLocal(rawConv)
1489	res.FinalizeInfo = rawConv.GetFinalizeInfo()
1490	res.SupersededBy = rawConv.SupersededBy
1491	res.Supersedes = rawConv.Supersedes
1492	res.IsEmpty = rawConv.IsEmpty
1493	res.Notifications = rawConv.Notifications
1494	res.CreatorInfo = rawConv.CreatorInfo
1495	res.TeamType = rawConv.Info.TeamType
1496	res.Version = rawConv.Info.Version
1497	res.LocalVersion = rawConv.Info.LocalVersion
1498	res.MaxMsgID = rawConv.ReaderInfo.MaxMsgid
1499	res.MaxVisibleMsgID = rawConv.MaxVisibleMsgID()
1500	res.ReadMsgID = rawConv.ReaderInfo.ReadMsgid
1501	res.ConvRetention = rawConv.ConvRetention
1502	res.TeamRetention = rawConv.TeamRetention
1503	res.ConvSettings = rawConv.ConvSettings
1504	res.Commands = rawConv.Commands
1505	res.BotCommands = rawConv.BotCommands
1506	res.BotAliases = rawConv.BotAliases
1507	res.Draft = rawConv.Info.Draft
1508	if rawConv.Info.PinnedMsg != nil {
1509		res.PinnedMsg = new(chat1.UIPinnedMessage)
1510		res.PinnedMsg.Message = PresentMessageUnboxed(ctx, g, rawConv.Info.PinnedMsg.Message, uid,
1511			rawConv.GetConvID())
1512		res.PinnedMsg.PinnerUsername = rawConv.Info.PinnedMsg.PinnerUsername
1513	}
1514	switch partMode {
1515	case PresentParticipantsModeInclude:
1516		res.Participants = PresentConversationParticipantsLocal(ctx, rawConv.Info.Participants)
1517	default:
1518	}
1519	return res
1520}
1521
1522func PresentConversationLocals(ctx context.Context, g *globals.Context, uid gregor1.UID,
1523	convs []chat1.ConversationLocal, partMode PresentParticipantsMode) (res []chat1.InboxUIItem) {
1524	res = make([]chat1.InboxUIItem, 0, len(convs))
1525	for _, conv := range convs {
1526		res = append(res, PresentConversationLocal(ctx, g, uid, conv, partMode))
1527	}
1528	return res
1529}
1530
1531func PresentThreadView(ctx context.Context, g *globals.Context, uid gregor1.UID, tv chat1.ThreadView,
1532	convID chat1.ConversationID) (res chat1.UIMessages) {
1533	res.Pagination = PresentPagination(tv.Pagination)
1534	res.Messages = make([]chat1.UIMessage, 0, len(tv.Messages))
1535	for _, msg := range tv.Messages {
1536		res.Messages = append(res.Messages, PresentMessageUnboxed(ctx, g, msg, uid, convID))
1537	}
1538	return res
1539}
1540
1541func computeOutboxOrdinal(obr chat1.OutboxRecord) float64 {
1542	return computeOrdinal(obr.Msg.ClientHeader.OutboxInfo.Prev, obr.Ordinal)
1543}
1544
1545// Compute an "ordinal". There are two senses of "ordinal".
1546// The service considers ordinals ints, like 3, which are the offset after some message ID.
1547// The frontend considers ordinals floats like "180.03" where before the dot is
1548// a message ID, and after the dot is a sub-position in thousandths.
1549// This function translates from the service's sense to the frontend's sense.
1550func computeOrdinal(messageID chat1.MessageID, serviceOrdinal int) (frontendOrdinal float64) {
1551	return float64(messageID) + float64(serviceOrdinal)/1000.0
1552}
1553
1554func PresentChannelNameMentions(ctx context.Context, crs []chat1.ChannelNameMention) (res []chat1.UIChannelNameMention) {
1555	res = make([]chat1.UIChannelNameMention, 0, len(crs))
1556	for _, cr := range crs {
1557		res = append(res, chat1.UIChannelNameMention{
1558			Name:   cr.TopicName,
1559			ConvID: cr.ConvID.ConvIDStr(),
1560		})
1561	}
1562	return res
1563}
1564
1565func formatVideoDuration(ms int) string {
1566	s := ms / 1000
1567	// see if we have hours
1568	if s >= 3600 {
1569		hours := s / 3600
1570		minutes := (s % 3600) / 60
1571		seconds := s - (hours*3600 + minutes*60)
1572		return fmt.Sprintf("%d:%02d:%02d", hours, minutes, seconds)
1573	}
1574	minutes := s / 60
1575	seconds := s % 60
1576	return fmt.Sprintf("%d:%02d", minutes, seconds)
1577}
1578
1579func PresentBytes(bytes int64) string {
1580	const (
1581		BYTE = 1.0 << (10 * iota)
1582		KILOBYTE
1583		MEGABYTE
1584		GIGABYTE
1585		TERABYTE
1586	)
1587	unit := ""
1588	value := float64(bytes)
1589	switch {
1590	case bytes >= TERABYTE:
1591		unit = "TB"
1592		value /= TERABYTE
1593	case bytes >= GIGABYTE:
1594		unit = "GB"
1595		value /= GIGABYTE
1596	case bytes >= MEGABYTE:
1597		unit = "MB"
1598		value /= MEGABYTE
1599	case bytes >= KILOBYTE:
1600		unit = "KB"
1601		value /= KILOBYTE
1602	case bytes >= BYTE:
1603		unit = "B"
1604	case bytes == 0:
1605		return "0"
1606	}
1607	return fmt.Sprintf("%.02f%s", value, unit)
1608}
1609
1610func formatVideoSize(bytes int64) string {
1611	return PresentBytes(bytes)
1612}
1613
1614func presentAttachmentAssetInfo(ctx context.Context, g *globals.Context, msg chat1.MessageUnboxed,
1615	convID chat1.ConversationID) *chat1.UIAssetUrlInfo {
1616	body := msg.Valid().MessageBody
1617	typ, err := body.MessageType()
1618	if err != nil {
1619		return nil
1620	}
1621	switch typ {
1622	case chat1.MessageType_ATTACHMENT, chat1.MessageType_ATTACHMENTUPLOADED:
1623		var hasFullURL, hasPreviewURL bool
1624		var asset chat1.Asset
1625		var info chat1.UIAssetUrlInfo
1626		if typ == chat1.MessageType_ATTACHMENT {
1627			asset = body.Attachment().Object
1628			info.MimeType = asset.MimeType
1629			hasFullURL = asset.Path != ""
1630			hasPreviewURL = body.Attachment().Preview != nil &&
1631				body.Attachment().Preview.Path != ""
1632		} else {
1633			asset = body.Attachmentuploaded().Object
1634			info.MimeType = asset.MimeType
1635			hasFullURL = asset.Path != ""
1636			hasPreviewURL = len(body.Attachmentuploaded().Previews) > 0 &&
1637				body.Attachmentuploaded().Previews[0].Path != ""
1638		}
1639		if hasFullURL {
1640			var cached bool
1641			info.FullUrl = g.AttachmentURLSrv.GetURL(ctx, convID, msg.GetMessageID(), false, false, false)
1642			cached, err = g.AttachmentURLSrv.GetAttachmentFetcher().IsAssetLocal(ctx, asset)
1643			if err != nil {
1644				cached = false
1645			}
1646			info.FullUrlCached = cached
1647		}
1648		if hasPreviewURL {
1649			info.PreviewUrl = g.AttachmentURLSrv.GetURL(ctx, convID, msg.GetMessageID(), true, false, false)
1650		}
1651		atyp, err := asset.Metadata.AssetType()
1652		if err == nil && atyp == chat1.AssetMetadataType_VIDEO && strings.HasPrefix(info.MimeType, "video") {
1653			if asset.Metadata.Video().DurationMs > 1 {
1654				info.VideoDuration = new(string)
1655				*info.VideoDuration = formatVideoDuration(asset.Metadata.Video().DurationMs) + ", " +
1656					formatVideoSize(asset.Size)
1657			}
1658			info.InlineVideoPlayable = true
1659		}
1660		if info.FullUrl == "" && info.PreviewUrl == "" && info.MimeType == "" {
1661			return nil
1662		}
1663		return &info
1664	}
1665	return nil
1666}
1667
1668func presentPaymentInfo(ctx context.Context, g *globals.Context, msgID chat1.MessageID,
1669	convID chat1.ConversationID, msg chat1.MessageUnboxedValid) []chat1.UIPaymentInfo {
1670	typ, err := msg.MessageBody.MessageType()
1671	if err != nil {
1672		return nil
1673	}
1674	var infos []chat1.UIPaymentInfo
1675	switch typ {
1676	case chat1.MessageType_SENDPAYMENT:
1677		body := msg.MessageBody.Sendpayment()
1678		info := g.StellarLoader.LoadPayment(ctx, convID, msgID, msg.SenderUsername, body.PaymentID)
1679		if info != nil {
1680			infos = []chat1.UIPaymentInfo{*info}
1681		}
1682	case chat1.MessageType_TEXT:
1683		body := msg.MessageBody.Text()
1684		// load any payments that were in the body of the text message
1685		for _, payment := range body.Payments {
1686			rtyp, err := payment.Result.ResultTyp()
1687			if err != nil {
1688				continue
1689			}
1690			switch rtyp {
1691			case chat1.TextPaymentResultTyp_SENT:
1692				paymentID := payment.Result.Sent()
1693				info := g.StellarLoader.LoadPayment(ctx, convID, msgID, msg.SenderUsername, paymentID)
1694				if info != nil {
1695					infos = append(infos, *info)
1696				}
1697			default:
1698				// Nothing to do for other payment result types.
1699			}
1700		}
1701	}
1702	for index := range infos {
1703		infos[index].Note = EscapeForDecorate(ctx, infos[index].Note)
1704	}
1705	return infos
1706}
1707
1708func presentRequestInfo(ctx context.Context, g *globals.Context, msgID chat1.MessageID,
1709	convID chat1.ConversationID, msg chat1.MessageUnboxedValid) *chat1.UIRequestInfo {
1710
1711	typ, err := msg.MessageBody.MessageType()
1712	if err != nil {
1713		return nil
1714	}
1715	switch typ {
1716	case chat1.MessageType_REQUESTPAYMENT:
1717		body := msg.MessageBody.Requestpayment()
1718		return g.StellarLoader.LoadRequest(ctx, convID, msgID, msg.SenderUsername, body.RequestID)
1719	default:
1720		// Nothing to do for other message types.
1721	}
1722	return nil
1723}
1724
1725func PresentUnfurl(ctx context.Context, g *globals.Context, convID chat1.ConversationID, u chat1.Unfurl) *chat1.UnfurlDisplay {
1726	ud, err := display.DisplayUnfurl(ctx, g.AttachmentURLSrv, convID, u)
1727	if err != nil {
1728		g.GetLog().CDebugf(ctx, "PresentUnfurl: failed to display unfurl: %s", err)
1729		return nil
1730	}
1731	return &ud
1732}
1733
1734func PresentUnfurls(ctx context.Context, g *globals.Context, uid gregor1.UID,
1735	convID chat1.ConversationID, unfurls map[chat1.MessageID]chat1.UnfurlResult) (res []chat1.UIMessageUnfurlInfo) {
1736	collapses := NewCollapses(g)
1737	res = make([]chat1.UIMessageUnfurlInfo, 0, len(unfurls))
1738	for unfurlMessageID, u := range unfurls {
1739		ud := PresentUnfurl(ctx, g, convID, u.Unfurl)
1740		if ud != nil {
1741			res = append(res, chat1.UIMessageUnfurlInfo{
1742				IsCollapsed: collapses.IsCollapsed(ctx, uid, convID, unfurlMessageID,
1743					chat1.MessageType_UNFURL),
1744				Unfurl:          *ud,
1745				UnfurlMessageID: unfurlMessageID,
1746				Url:             u.Url,
1747			})
1748		}
1749	}
1750	return res
1751}
1752
1753func PresentDecoratedReactionMap(ctx context.Context, g *globals.Context, uid gregor1.UID,
1754	convID chat1.ConversationID, msg chat1.MessageUnboxedValid, reactions chat1.ReactionMap) (res chat1.UIReactionMap) {
1755	shouldDecorate := len(msg.Emojis) > 0
1756	res.Reactions = make(map[string]chat1.UIReactionDesc, len(reactions.Reactions))
1757	for key, value := range reactions.Reactions {
1758		var desc chat1.UIReactionDesc
1759		if shouldDecorate {
1760			desc.Decorated = g.EmojiSource.Decorate(ctx, key, uid,
1761				chat1.MessageType_REACTION, msg.Emojis)
1762		}
1763		desc.Users = make(map[string]chat1.Reaction)
1764		for username, reaction := range value {
1765			desc.Users[username] = reaction
1766		}
1767		res.Reactions[key] = desc
1768	}
1769	return res
1770}
1771
1772func PresentDecoratedUserBio(ctx context.Context, bio string) (res string) {
1773	res = EscapeForDecorate(ctx, bio)
1774	res = EscapeShrugs(ctx, res)
1775	res = DecorateWithLinks(ctx, res)
1776	return res
1777}
1778
1779func systemMsgPresentText(ctx context.Context, uid gregor1.UID, msg chat1.MessageUnboxedValid) string {
1780	if !msg.MessageBody.IsType(chat1.MessageType_SYSTEM) {
1781		return ""
1782	}
1783	sysMsg := msg.MessageBody.System()
1784	typ, err := sysMsg.SystemType()
1785	if err != nil {
1786		return ""
1787	}
1788	switch typ {
1789	case chat1.MessageSystemType_NEWCHANNEL:
1790		var author string
1791		if uid.Eq(msg.ClientHeader.Sender) {
1792			author = "You "
1793		}
1794		if len(msg.ChannelNameMentions) == 1 {
1795			return fmt.Sprintf("%screated a new channel #%s", author, msg.ChannelNameMentions[0].TopicName)
1796		} else if len(msg.ChannelNameMentions) > 1 {
1797			return fmt.Sprintf("%screated #%s and %d other new channels", author, msg.ChannelNameMentions[0].TopicName, len(sysMsg.Newchannel().ConvIDs)-1)
1798		}
1799	default:
1800	}
1801	return ""
1802}
1803
1804func PresentDecoratedTextNoMentions(ctx context.Context, body string) string {
1805	// escape before applying xforms
1806	body = EscapeForDecorate(ctx, body)
1807	body = EscapeShrugs(ctx, body)
1808
1809	// This needs to happen before (deep) links.
1810	kbfsPaths := ParseKBFSPaths(ctx, body)
1811	body = DecorateWithKBFSPath(ctx, body, kbfsPaths)
1812
1813	// Links
1814	body = DecorateWithLinks(ctx, body)
1815	return body
1816}
1817
1818func PresentDecoratedSnippet(ctx context.Context, g *globals.Context, body string,
1819	uid gregor1.UID, msgType chat1.MessageType, emojis []chat1.HarvestedEmoji) string {
1820	body = EscapeForDecorate(ctx, body)
1821	body = EscapeShrugs(ctx, body)
1822	return g.EmojiSource.Decorate(ctx, body, uid, msgType, emojis)
1823}
1824
1825func PresentDecoratedPendingTextBody(ctx context.Context, g *globals.Context, uid gregor1.UID,
1826	msg chat1.MessagePlaintext) *string {
1827	typ, err := msg.MessageBody.MessageType()
1828	if err != nil {
1829		return nil
1830	}
1831	body := msg.MessageBody.TextForDecoration()
1832	body = PresentDecoratedTextNoMentions(ctx, body)
1833	body = g.EmojiSource.Decorate(ctx, body, uid, typ, msg.Emojis)
1834	return &body
1835}
1836
1837func PresentDecoratedTextBody(ctx context.Context, g *globals.Context, uid gregor1.UID,
1838	convID chat1.ConversationID, msg chat1.MessageUnboxedValid) *string {
1839	msgBody := msg.MessageBody
1840	typ, err := msgBody.MessageType()
1841	if err != nil {
1842		return nil
1843	}
1844	body := msgBody.TextForDecoration()
1845	if len(body) == 0 {
1846		return nil
1847	}
1848	var payments []chat1.TextPayment
1849	switch typ {
1850	case chat1.MessageType_TEXT:
1851		payments = msgBody.Text().Payments
1852	case chat1.MessageType_SYSTEM:
1853		body = systemMsgPresentText(ctx, uid, msg)
1854	}
1855
1856	body = PresentDecoratedTextNoMentions(ctx, body)
1857	// Payments
1858	body = g.StellarSender.DecorateWithPayments(ctx, body, payments)
1859	// Emojis
1860	body = g.EmojiSource.Decorate(ctx, body, uid, typ, msg.Emojis)
1861	// Mentions
1862	body = DecorateWithMentions(ctx, body, msg.AtMentionUsernames, msg.MaybeMentions, msg.ChannelMention,
1863		msg.ChannelNameMentions)
1864	return &body
1865}
1866
1867func loadTeamMentions(ctx context.Context, g *globals.Context, uid gregor1.UID,
1868	valid chat1.MessageUnboxedValid) {
1869	var knownTeamMentions []chat1.KnownTeamMention
1870	typ, err := valid.MessageBody.MessageType()
1871	if err != nil {
1872		return
1873	}
1874	switch typ {
1875	case chat1.MessageType_TEXT:
1876		knownTeamMentions = valid.MessageBody.Text().TeamMentions
1877	case chat1.MessageType_FLIP:
1878		knownTeamMentions = valid.MessageBody.Flip().TeamMentions
1879	case chat1.MessageType_EDIT:
1880		knownTeamMentions = valid.MessageBody.Edit().TeamMentions
1881	}
1882	for _, tm := range valid.MaybeMentions {
1883		if err := g.TeamMentionLoader.LoadTeamMention(ctx, uid, tm, knownTeamMentions, false); err != nil {
1884			g.GetLog().CDebugf(ctx, "loadTeamMentions: error loading team mentions: %+v", err)
1885		}
1886	}
1887}
1888
1889func presentFlipGameID(ctx context.Context, g *globals.Context, uid gregor1.UID,
1890	convID chat1.ConversationID, msg chat1.MessageUnboxed) *chat1.FlipGameIDStr {
1891	typ, err := msg.State()
1892	if err != nil {
1893		return nil
1894	}
1895	var body chat1.MessageBody
1896	switch typ {
1897	case chat1.MessageUnboxedState_VALID:
1898		body = msg.Valid().MessageBody
1899	case chat1.MessageUnboxedState_OUTBOX:
1900		body = msg.Outbox().Msg.MessageBody
1901	default:
1902		return nil
1903	}
1904	if !body.IsType(chat1.MessageType_FLIP) {
1905		return nil
1906	}
1907	if msg.GetTopicType() == chat1.TopicType_CHAT && !msg.IsOutbox() {
1908		// only queue up a flip load for the flip messages in chat channels
1909		g.CoinFlipManager.LoadFlip(ctx, uid, convID, msg.GetMessageID(), body.Flip().FlipConvID,
1910			body.Flip().GameID)
1911	}
1912	ret := body.Flip().GameID.FlipGameIDStr()
1913	return &ret
1914}
1915
1916func PresentMessagesUnboxed(ctx context.Context, g *globals.Context, msgs []chat1.MessageUnboxed,
1917	uid gregor1.UID, convID chat1.ConversationID) (res []chat1.UIMessage) {
1918	res = make([]chat1.UIMessage, 0, len(msgs))
1919	for _, msg := range msgs {
1920		res = append(res, PresentMessageUnboxed(ctx, g, msg, uid, convID))
1921	}
1922	return res
1923}
1924
1925func PresentMessageUnboxed(ctx context.Context, g *globals.Context, rawMsg chat1.MessageUnboxed,
1926	uid gregor1.UID, convID chat1.ConversationID) (res chat1.UIMessage) {
1927	miscErr := func(err error) chat1.UIMessage {
1928		return chat1.NewUIMessageWithError(chat1.MessageUnboxedError{
1929			ErrType:   chat1.MessageUnboxedErrorType_MISC,
1930			ErrMsg:    err.Error(),
1931			MessageID: rawMsg.GetMessageID(),
1932		})
1933	}
1934
1935	collapses := NewCollapses(g)
1936	state, err := rawMsg.State()
1937	if err != nil {
1938		return miscErr(err)
1939	}
1940	switch state {
1941	case chat1.MessageUnboxedState_VALID:
1942		valid := rawMsg.Valid()
1943		if !rawMsg.IsValidFull() {
1944			// If we have an expired ephemeral message, don't show an error
1945			// message.
1946			if !(valid.IsEphemeral() && valid.IsEphemeralExpired(time.Now())) {
1947				return miscErr(fmt.Errorf("unexpected deleted %v message",
1948					strings.ToLower(rawMsg.GetMessageType().String())))
1949			}
1950		}
1951		var strOutboxID *string
1952		if valid.ClientHeader.OutboxID != nil {
1953			so := valid.ClientHeader.OutboxID.String()
1954			strOutboxID = &so
1955		}
1956		var replyTo *chat1.UIMessage
1957		if valid.ReplyTo != nil {
1958			replyTo = new(chat1.UIMessage)
1959			*replyTo = PresentMessageUnboxed(ctx, g, *valid.ReplyTo, uid, convID)
1960		}
1961		var pinnedMessageID *chat1.MessageID
1962		if valid.MessageBody.IsType(chat1.MessageType_PIN) {
1963			pinnedMessageID = new(chat1.MessageID)
1964			*pinnedMessageID = valid.MessageBody.Pin().MsgID
1965		}
1966		loadTeamMentions(ctx, g, uid, valid)
1967		bodySummary, _ := GetMsgSnippetBody(ctx, g, uid, convID, rawMsg)
1968		res = chat1.NewUIMessageWithValid(chat1.UIMessageValid{
1969			MessageID:             rawMsg.GetMessageID(),
1970			Ctime:                 valid.ServerHeader.Ctime,
1971			OutboxID:              strOutboxID,
1972			MessageBody:           valid.MessageBody,
1973			DecoratedTextBody:     PresentDecoratedTextBody(ctx, g, uid, convID, valid),
1974			BodySummary:           bodySummary,
1975			SenderUsername:        valid.SenderUsername,
1976			SenderDeviceName:      valid.SenderDeviceName,
1977			SenderDeviceType:      valid.SenderDeviceType,
1978			SenderDeviceRevokedAt: valid.SenderDeviceRevokedAt,
1979			SenderUID:             valid.ClientHeader.Sender,
1980			SenderDeviceID:        valid.ClientHeader.SenderDevice,
1981			Superseded:            valid.ServerHeader.SupersededBy != 0,
1982			AtMentions:            valid.AtMentionUsernames,
1983			ChannelMention:        valid.ChannelMention,
1984			ChannelNameMentions:   PresentChannelNameMentions(ctx, valid.ChannelNameMentions),
1985			AssetUrlInfo:          presentAttachmentAssetInfo(ctx, g, rawMsg, convID),
1986			IsEphemeral:           valid.IsEphemeral(),
1987			IsEphemeralExpired:    valid.IsEphemeralExpired(time.Now()),
1988			ExplodedBy:            valid.ExplodedBy(),
1989			Etime:                 valid.Etime(),
1990			Reactions:             PresentDecoratedReactionMap(ctx, g, uid, convID, valid, valid.Reactions),
1991			HasPairwiseMacs:       valid.HasPairwiseMacs(),
1992			FlipGameID:            presentFlipGameID(ctx, g, uid, convID, rawMsg),
1993			PaymentInfos:          presentPaymentInfo(ctx, g, rawMsg.GetMessageID(), convID, valid),
1994			RequestInfo:           presentRequestInfo(ctx, g, rawMsg.GetMessageID(), convID, valid),
1995			Unfurls:               PresentUnfurls(ctx, g, uid, convID, valid.Unfurls),
1996			IsDeleteable:          IsDeleteableByDeleteMessageType(valid),
1997			IsEditable:            IsEditableByEditMessageType(rawMsg.GetMessageType()),
1998			ReplyTo:               replyTo,
1999			PinnedMessageID:       pinnedMessageID,
2000			BotUsername:           valid.BotUsername,
2001			IsCollapsed: collapses.IsCollapsed(ctx, uid, convID, rawMsg.GetMessageID(),
2002				rawMsg.GetMessageType()),
2003		})
2004	case chat1.MessageUnboxedState_OUTBOX:
2005		var body, title, filename string
2006		var decoratedBody *string
2007		var preview *chat1.MakePreviewRes
2008		typ := rawMsg.Outbox().Msg.ClientHeader.MessageType
2009		switch typ {
2010		case chat1.MessageType_TEXT:
2011			body = rawMsg.Outbox().Msg.MessageBody.Text().Body
2012			decoratedBody = PresentDecoratedPendingTextBody(ctx, g, uid, rawMsg.Outbox().Msg)
2013		case chat1.MessageType_FLIP:
2014			body = rawMsg.Outbox().Msg.MessageBody.Flip().Text
2015			decoratedBody = new(string)
2016			*decoratedBody = EscapeShrugs(ctx, body)
2017		case chat1.MessageType_EDIT:
2018			body = rawMsg.Outbox().Msg.MessageBody.Edit().Body
2019		case chat1.MessageType_ATTACHMENT:
2020			preview = rawMsg.Outbox().Preview
2021			msgBody := rawMsg.Outbox().Msg.MessageBody
2022			btyp, err := msgBody.MessageType()
2023			if err == nil && btyp == chat1.MessageType_ATTACHMENT {
2024				asset := msgBody.Attachment().Object
2025				title = asset.Title
2026				filename = asset.Filename
2027			}
2028		case chat1.MessageType_REACTION:
2029			body = rawMsg.Outbox().Msg.MessageBody.Reaction().Body
2030			decoratedBody = PresentDecoratedPendingTextBody(ctx, g, uid, rawMsg.Outbox().Msg)
2031		}
2032		var replyTo *chat1.UIMessage
2033		if rawMsg.Outbox().ReplyTo != nil {
2034			replyTo = new(chat1.UIMessage)
2035			*replyTo = PresentMessageUnboxed(ctx, g, *rawMsg.Outbox().ReplyTo, uid, convID)
2036		}
2037		res = chat1.NewUIMessageWithOutbox(chat1.UIMessageOutbox{
2038			State:             rawMsg.Outbox().State,
2039			OutboxID:          rawMsg.Outbox().OutboxID.String(),
2040			MessageType:       typ,
2041			Body:              body,
2042			DecoratedTextBody: decoratedBody,
2043			Ctime:             rawMsg.Outbox().Ctime,
2044			Ordinal:           computeOutboxOrdinal(rawMsg.Outbox()),
2045			Preview:           preview,
2046			Title:             title,
2047			Filename:          filename,
2048			IsEphemeral:       rawMsg.Outbox().Msg.IsEphemeral(),
2049			FlipGameID:        presentFlipGameID(ctx, g, uid, convID, rawMsg),
2050			ReplyTo:           replyTo,
2051			Supersedes:        rawMsg.Outbox().Msg.ClientHeader.Supersedes,
2052		})
2053	case chat1.MessageUnboxedState_ERROR:
2054		res = chat1.NewUIMessageWithError(rawMsg.Error())
2055	case chat1.MessageUnboxedState_PLACEHOLDER:
2056		res = chat1.NewUIMessageWithPlaceholder(rawMsg.Placeholder())
2057	case chat1.MessageUnboxedState_JOURNEYCARD:
2058		journeycard := rawMsg.Journeycard()
2059		res = chat1.NewUIMessageWithJourneycard(chat1.UIMessageJourneycard{
2060			Ordinal:        computeOrdinal(journeycard.PrevID, journeycard.Ordinal),
2061			CardType:       journeycard.CardType,
2062			HighlightMsgID: journeycard.HighlightMsgID,
2063			OpenTeam:       journeycard.OpenTeam,
2064		})
2065	default:
2066		g.MetaContext(ctx).Debug("PresentMessageUnboxed: unhandled MessageUnboxedState: %v", state)
2067		// res = zero values
2068	}
2069	return res
2070}
2071
2072func PresentPagination(p *chat1.Pagination) (res *chat1.UIPagination) {
2073	if p == nil {
2074		return nil
2075	}
2076	res = new(chat1.UIPagination)
2077	res.Last = p.Last
2078	res.Num = p.Num
2079	res.Next = hex.EncodeToString(p.Next)
2080	res.Previous = hex.EncodeToString(p.Previous)
2081	return res
2082}
2083
2084func DecodePagination(p *chat1.UIPagination) (res *chat1.Pagination, err error) {
2085	if p == nil {
2086		return nil, nil
2087	}
2088	res = new(chat1.Pagination)
2089	res.Last = p.Last
2090	res.Num = p.Num
2091	if res.Next, err = hex.DecodeString(p.Next); err != nil {
2092		return nil, err
2093	}
2094	if res.Previous, err = hex.DecodeString(p.Previous); err != nil {
2095		return nil, err
2096	}
2097	return res, nil
2098}
2099
2100type ConvLocalByConvID []chat1.ConversationLocal
2101
2102func (c ConvLocalByConvID) Len() int      { return len(c) }
2103func (c ConvLocalByConvID) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
2104func (c ConvLocalByConvID) Less(i, j int) bool {
2105	return c[i].GetConvID().Less(c[j].GetConvID())
2106}
2107
2108type ConvByConvID []chat1.Conversation
2109
2110func (c ConvByConvID) Len() int      { return len(c) }
2111func (c ConvByConvID) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
2112func (c ConvByConvID) Less(i, j int) bool {
2113	return c[i].GetConvID().Less(c[j].GetConvID())
2114}
2115
2116type RemoteConvByConvID []types.RemoteConversation
2117
2118func (c RemoteConvByConvID) Len() int      { return len(c) }
2119func (c RemoteConvByConvID) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
2120func (c RemoteConvByConvID) Less(i, j int) bool {
2121	return c[i].GetConvID().Less(c[j].GetConvID())
2122}
2123
2124type RemoteConvByMtime []types.RemoteConversation
2125
2126func (c RemoteConvByMtime) Len() int      { return len(c) }
2127func (c RemoteConvByMtime) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
2128func (c RemoteConvByMtime) Less(i, j int) bool {
2129	return GetConvMtime(c[i]) > GetConvMtime(c[j])
2130}
2131
2132type ConvLocalByTopicName []chat1.ConversationLocal
2133
2134func (c ConvLocalByTopicName) Len() int      { return len(c) }
2135func (c ConvLocalByTopicName) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
2136func (c ConvLocalByTopicName) Less(i, j int) bool {
2137	return c[i].Info.TopicName < c[j].Info.TopicName
2138}
2139
2140type ByConvID []chat1.ConversationID
2141
2142func (c ByConvID) Len() int      { return len(c) }
2143func (c ByConvID) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
2144func (c ByConvID) Less(i, j int) bool {
2145	return c[i].Less(c[j])
2146}
2147
2148type ByMsgSummaryCtime []chat1.MessageSummary
2149
2150func (c ByMsgSummaryCtime) Len() int      { return len(c) }
2151func (c ByMsgSummaryCtime) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
2152func (c ByMsgSummaryCtime) Less(i, j int) bool {
2153	return c[i].Ctime.Before(c[j].Ctime)
2154}
2155
2156type ByMsgUnboxedCtime []chat1.MessageUnboxed
2157
2158func (c ByMsgUnboxedCtime) Len() int      { return len(c) }
2159func (c ByMsgUnboxedCtime) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
2160func (c ByMsgUnboxedCtime) Less(i, j int) bool {
2161	return c[i].Valid().ServerHeader.Ctime.Before(c[j].Valid().ServerHeader.Ctime)
2162}
2163
2164type ByMsgUnboxedMsgID []chat1.MessageUnboxed
2165
2166func (c ByMsgUnboxedMsgID) Len() int      { return len(c) }
2167func (c ByMsgUnboxedMsgID) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
2168func (c ByMsgUnboxedMsgID) Less(i, j int) bool {
2169	return c[i].GetMessageID() > c[j].GetMessageID()
2170}
2171
2172type ByMsgID []chat1.MessageID
2173
2174func (m ByMsgID) Len() int           { return len(m) }
2175func (m ByMsgID) Swap(i, j int)      { m[i], m[j] = m[j], m[i] }
2176func (m ByMsgID) Less(i, j int) bool { return m[i] > m[j] }
2177
2178func NotificationInfoSet(settings *chat1.ConversationNotificationInfo,
2179	apptype keybase1.DeviceType,
2180	kind chat1.NotificationKind, enabled bool) {
2181	if settings.Settings == nil {
2182		settings.Settings = make(map[keybase1.DeviceType]map[chat1.NotificationKind]bool)
2183	}
2184	if settings.Settings[apptype] == nil {
2185		settings.Settings[apptype] = make(map[chat1.NotificationKind]bool)
2186	}
2187	settings.Settings[apptype][kind] = enabled
2188}
2189
2190func DecodeBase64(enc []byte) ([]byte, error) {
2191	if len(enc) == 0 {
2192		return enc, nil
2193	}
2194
2195	b := make([]byte, base64.StdEncoding.DecodedLen(len(enc)))
2196	n, err := base64.StdEncoding.Decode(b, enc)
2197	return b[:n], err
2198}
2199
2200func RemoteConv(conv chat1.Conversation) types.RemoteConversation {
2201	return types.RemoteConversation{
2202		Conv:      conv,
2203		ConvIDStr: conv.GetConvID().ConvIDStr(),
2204	}
2205}
2206
2207func RemoteConvs(convs []chat1.Conversation) (res []types.RemoteConversation) {
2208	res = make([]types.RemoteConversation, 0, len(convs))
2209	for _, conv := range convs {
2210		res = append(res, RemoteConv(conv))
2211	}
2212	return res
2213}
2214
2215func PluckConvs(rcs []types.RemoteConversation) (res []chat1.Conversation) {
2216	res = make([]chat1.Conversation, 0, len(rcs))
2217	for _, rc := range rcs {
2218		res = append(res, rc.Conv)
2219	}
2220	return res
2221}
2222
2223func SplitTLFName(tlfName string) []string {
2224	return strings.Split(strings.Fields(tlfName)[0], ",")
2225}
2226
2227func UsernamePackageToParticipant(p libkb.UsernamePackage) chat1.ConversationLocalParticipant {
2228	var fullName *string
2229	if p.FullName != nil {
2230		s := string(p.FullName.FullName)
2231		fullName = &s
2232	}
2233	return chat1.ConversationLocalParticipant{
2234		Username: p.NormalizedUsername.String(),
2235		Fullname: fullName,
2236	}
2237}
2238
2239type pagerMsg struct {
2240	msgID chat1.MessageID
2241}
2242
2243func (p pagerMsg) GetMessageID() chat1.MessageID {
2244	return p.msgID
2245}
2246
2247func MessageIDControlToPagination(ctx context.Context, logger DebugLabeler, control *chat1.MessageIDControl,
2248	conv *types.RemoteConversation) (res *chat1.Pagination) {
2249	if control == nil {
2250		return res
2251	}
2252	pag := pager.NewThreadPager()
2253	res = new(chat1.Pagination)
2254	res.Num = control.Num
2255	if control.Pivot != nil {
2256		var err error
2257		pm := pagerMsg{msgID: *control.Pivot}
2258		switch control.Mode {
2259		case chat1.MessageIDControlMode_OLDERMESSAGES:
2260			res.Next, err = pag.MakeIndex(pm)
2261		case chat1.MessageIDControlMode_NEWERMESSAGES:
2262			res.Previous, err = pag.MakeIndex(pm)
2263		case chat1.MessageIDControlMode_UNREADLINE:
2264			if conv == nil {
2265				// just bail out of here with no conversation
2266				logger.Debug(ctx, "MessageIDControlToPagination: unreadline mode with no conv, bailing")
2267				return nil
2268			}
2269			pm.msgID = conv.Conv.ReaderInfo.ReadMsgid
2270			fallthrough
2271		case chat1.MessageIDControlMode_CENTERED:
2272			// Heuristic that we might want to revisit, get older messages from a little ahead of where
2273			// we want to center on
2274			if conv == nil {
2275				// just bail out of here with no conversation
2276				logger.Debug(ctx, "MessageIDControlToPagination: centered mode with no conv, bailing")
2277				return nil
2278			}
2279			maxID := int(conv.Conv.MaxVisibleMsgID())
2280			desired := int(pm.msgID) + control.Num/2
2281			logger.Debug(ctx, "MessageIDControlToPagination: maxID: %d desired: %d", maxID, desired)
2282			if desired > maxID {
2283				desired = maxID
2284			}
2285			pm.msgID = chat1.MessageID(desired + 1)
2286			res.Next, err = pag.MakeIndex(pm)
2287			res.ForceFirstPage = true
2288		}
2289		if err != nil {
2290			return nil
2291		}
2292	}
2293	return res
2294}
2295
2296// AssetsForMessage gathers all assets on a message
2297func AssetsForMessage(g *globals.Context, msgBody chat1.MessageBody) (assets []chat1.Asset) {
2298	typ, err := msgBody.MessageType()
2299	if err != nil {
2300		// Log and drop the error for a malformed MessageBody.
2301		g.Log.Warning("error getting assets for message: %s", err)
2302		return assets
2303	}
2304	switch typ {
2305	case chat1.MessageType_ATTACHMENT:
2306		body := msgBody.Attachment()
2307		if body.Object.Path != "" {
2308			assets = append(assets, body.Object)
2309		}
2310		if body.Preview != nil {
2311			assets = append(assets, *body.Preview)
2312		}
2313		assets = append(assets, body.Previews...)
2314	case chat1.MessageType_ATTACHMENTUPLOADED:
2315		body := msgBody.Attachmentuploaded()
2316		if body.Object.Path != "" {
2317			assets = append(assets, body.Object)
2318		}
2319		assets = append(assets, body.Previews...)
2320	}
2321	return assets
2322}
2323
2324func AddUserToTLFName(g *globals.Context, tlfName string, vis keybase1.TLFVisibility,
2325	membersType chat1.ConversationMembersType) string {
2326	switch membersType {
2327	case chat1.ConversationMembersType_IMPTEAMNATIVE, chat1.ConversationMembersType_IMPTEAMUPGRADE,
2328		chat1.ConversationMembersType_KBFS:
2329		if vis == keybase1.TLFVisibility_PUBLIC {
2330			return tlfName
2331		}
2332
2333		username := g.Env.GetUsername().String()
2334		if len(tlfName) == 0 {
2335			return username
2336		}
2337
2338		// KBFS creates TLFs with suffixes (e.g., folder names that
2339		// conflict after an assertion has been resolved) and readers,
2340		// so we need to handle those types of TLF names here so that
2341		// edit history works correctly.
2342		split1 := strings.SplitN(tlfName, " ", 2) // split off suffix
2343		split2 := strings.Split(split1[0], "#")   // split off readers
2344		// Add the name to the writers list (assume the current user
2345		// is a writer).
2346		tlfName = split2[0] + "," + username
2347		if len(split2) > 1 {
2348			// Re-append any readers.
2349			tlfName += "#" + split2[1]
2350		}
2351		if len(split1) > 1 {
2352			// Re-append any suffix.
2353			tlfName += " " + split1[1]
2354		}
2355		return tlfName
2356	default:
2357		return tlfName
2358	}
2359}
2360
2361func ForceReloadUPAKsForUIDs(ctx context.Context, g *globals.Context, uids []keybase1.UID) error {
2362	getArg := func(i int) *libkb.LoadUserArg {
2363		if i >= len(uids) {
2364			return nil
2365		}
2366		tmp := libkb.NewLoadUserByUIDForceArg(g.GlobalContext, uids[i])
2367		return &tmp
2368	}
2369	return g.GetUPAKLoader().Batcher(ctx, getArg, nil, 0)
2370}
2371
2372func CreateHiddenPlaceholder(msgID chat1.MessageID) chat1.MessageUnboxed {
2373	return chat1.NewMessageUnboxedWithPlaceholder(
2374		chat1.MessageUnboxedPlaceholder{
2375			MessageID: msgID,
2376			Hidden:    true,
2377		})
2378}
2379
2380func GetGregorConn(ctx context.Context, g *globals.Context, log DebugLabeler,
2381	handler func(nist *libkb.NIST) rpc.ConnectionHandler) (conn *rpc.Connection, token gregor1.SessionToken, err error) {
2382	// Get session token
2383	nist, _, _, err := g.ActiveDevice.NISTAndUIDDeviceID(ctx)
2384	if nist == nil {
2385		log.Debug(ctx, "GetGregorConn: got a nil NIST, is the user logged out?")
2386		return conn, token, libkb.LoggedInError{}
2387	}
2388	if err != nil {
2389		log.Debug(ctx, "GetGregorConn: failed to get logged in session: %s", err.Error())
2390		return conn, token, err
2391	}
2392	token = gregor1.SessionToken(nist.Token().String())
2393
2394	// Make an ad hoc connection to gregor
2395	uri, err := rpc.ParseFMPURI(g.Env.GetGregorURI())
2396	if err != nil {
2397		log.Debug(ctx, "GetGregorConn: failed to parse chat server UR: %s", err.Error())
2398		return conn, token, err
2399	}
2400
2401	if uri.UseTLS() {
2402		rawCA := g.Env.GetBundledCA(uri.Host)
2403		if len(rawCA) == 0 {
2404			err := errors.New("len(rawCA) == 0")
2405			log.Debug(ctx, "GetGregorConn: failed to parse CAs", err.Error())
2406			return conn, token, err
2407		}
2408		conn = rpc.NewTLSConnectionWithDialable(rpc.NewFixedRemote(uri.HostPort),
2409			[]byte(rawCA), libkb.NewContextifiedErrorUnwrapper(g.ExternalG()),
2410			handler(nist), libkb.NewRPCLogFactory(g.ExternalG()),
2411			g.ExternalG().RemoteNetworkInstrumenterStorage,
2412			logger.LogOutputWithDepthAdder{Logger: g.Log},
2413			rpc.DefaultMaxFrameLength, rpc.ConnectionOpts{},
2414			libkb.NewProxyDialable(g.Env))
2415	} else {
2416		t := rpc.NewConnectionTransportWithDialable(uri, nil,
2417			g.ExternalG().RemoteNetworkInstrumenterStorage,
2418			libkb.MakeWrapError(g.ExternalG()),
2419			rpc.DefaultMaxFrameLength, libkb.NewProxyDialable(g.GetEnv()))
2420		conn = rpc.NewConnectionWithTransport(handler(nist), t,
2421			libkb.NewContextifiedErrorUnwrapper(g.ExternalG()),
2422			logger.LogOutputWithDepthAdder{Logger: g.Log}, rpc.ConnectionOpts{})
2423	}
2424	return conn, token, nil
2425}
2426
2427// GetQueryRe returns a regex to match the query string on message text. This
2428// is used for result highlighting.
2429func GetQueryRe(query string) (*regexp.Regexp, error) {
2430	return regexp.Compile("(?i)" + regexp.QuoteMeta(query))
2431}
2432
2433func SetUnfurl(mvalid *chat1.MessageUnboxedValid, unfurlMessageID chat1.MessageID,
2434	unfurl chat1.UnfurlResult) {
2435	if mvalid.Unfurls == nil {
2436		mvalid.Unfurls = make(map[chat1.MessageID]chat1.UnfurlResult)
2437	}
2438	mvalid.Unfurls[unfurlMessageID] = unfurl
2439}
2440
2441func RemoveUnfurl(mvalid *chat1.MessageUnboxedValid, unfurlMessageID chat1.MessageID) {
2442	if mvalid.Unfurls == nil {
2443		return
2444	}
2445	delete(mvalid.Unfurls, unfurlMessageID)
2446}
2447
2448// SuspendComponent will suspend a Suspendable type until the return function
2449// is called. This allows a succinct call like defer SuspendComponent(ctx, g,
2450// g.ConvLoader)() in RPC handlers wishing to lock out the conv loader.
2451func SuspendComponent(ctx context.Context, g *globals.Context, suspendable types.Suspendable) func() {
2452	if canceled := suspendable.Suspend(ctx); canceled {
2453		g.Log.CDebugf(ctx, "SuspendComponent: canceled background task")
2454	}
2455	return func() {
2456		suspendable.Resume(ctx)
2457	}
2458}
2459
2460func SuspendComponents(ctx context.Context, g *globals.Context, suspendables []types.Suspendable) func() {
2461	resumeFuncs := make([]func(), 0, len(suspendables))
2462	for _, s := range suspendables {
2463		resumeFuncs = append(resumeFuncs, SuspendComponent(ctx, g, s))
2464	}
2465	return func() {
2466		for _, f := range resumeFuncs {
2467			f()
2468		}
2469	}
2470}
2471
2472func IsPermanentErr(err error) bool {
2473	if uberr, ok := err.(types.UnboxingError); ok {
2474		return uberr.IsPermanent()
2475	}
2476	return err != nil
2477}
2478
2479func EphemeralLifetimeFromConv(ctx context.Context, g *globals.Context, conv chat1.ConversationLocal) (res *gregor1.DurationSec, err error) {
2480	// Check to see if the conversation has an exploding policy
2481	var retentionRes *gregor1.DurationSec
2482	var gregorRes *gregor1.DurationSec
2483	var rentTyp chat1.RetentionPolicyType
2484	var convSet bool
2485	if conv.ConvRetention != nil {
2486		if rentTyp, err = conv.ConvRetention.Typ(); err != nil {
2487			return res, err
2488		}
2489		if rentTyp == chat1.RetentionPolicyType_EPHEMERAL {
2490			e := conv.ConvRetention.Ephemeral()
2491			retentionRes = &e.Age
2492		}
2493		convSet = rentTyp != chat1.RetentionPolicyType_INHERIT
2494	}
2495	if !convSet && conv.TeamRetention != nil {
2496		if rentTyp, err = conv.TeamRetention.Typ(); err != nil {
2497			return res, err
2498		}
2499		if rentTyp == chat1.RetentionPolicyType_EPHEMERAL {
2500			e := conv.TeamRetention.Ephemeral()
2501			retentionRes = &e.Age
2502		}
2503	}
2504
2505	// See if there is anything in Gregor
2506	st, err := g.GregorState.State(ctx)
2507	if err != nil {
2508		return res, err
2509	}
2510	// Note: this value is present on the JS frontend as well
2511	key := fmt.Sprintf("exploding:%s", conv.GetConvID())
2512	cat, err := gregor1.ObjFactory{}.MakeCategory(key)
2513	if err != nil {
2514		return res, err
2515	}
2516	items, err := st.ItemsWithCategoryPrefix(cat)
2517	if err != nil {
2518		return res, err
2519	}
2520	if len(items) > 0 {
2521		it := items[0]
2522		body := string(it.Body().Bytes())
2523		sec, err := strconv.ParseInt(body, 0, 0)
2524		if err != nil {
2525			return res, nil
2526		}
2527		gsec := gregor1.DurationSec(sec)
2528		gregorRes = &gsec
2529	}
2530	if retentionRes != nil && gregorRes != nil {
2531		if *gregorRes < *retentionRes {
2532			return gregorRes, nil
2533		}
2534		return retentionRes, nil
2535	} else if retentionRes != nil {
2536		return retentionRes, nil
2537	} else if gregorRes != nil {
2538		return gregorRes, nil
2539	} else {
2540		return nil, nil
2541	}
2542}
2543
2544var decorateBegin = "$>kb$"
2545var decorateEnd = "$<kb$"
2546var decorateEscapeRe = regexp.MustCompile(`\\*\$\>kb\$`)
2547
2548func EscapeForDecorate(ctx context.Context, body string) string {
2549	// escape any natural occurrences of begin so we don't bust markdown parser
2550	return decorateEscapeRe.ReplaceAllStringFunc(body, func(s string) string {
2551		if len(s)%2 != 0 {
2552			return `\` + s
2553		}
2554		return s
2555	})
2556}
2557
2558func DecorateBody(ctx context.Context, body string, offset, length int, decoration interface{}) (res string, added int) {
2559	out, err := json.Marshal(decoration)
2560	if err != nil {
2561		return res, 0
2562	}
2563	//b64out := string(out)
2564	b64out := base64.StdEncoding.EncodeToString(out)
2565	strDecoration := fmt.Sprintf("%s%s%s", decorateBegin, b64out, decorateEnd)
2566	added = len(strDecoration) - length
2567	res = fmt.Sprintf("%s%s%s", body[:offset], strDecoration, body[offset+length:])
2568	return res, added
2569}
2570
2571var linkRegexp = xurls.Relaxed()
2572
2573// These indices correspond to the named capture groups in the xurls regexes
2574var linkRelaxedGroupIndex = 0
2575var linkStrictGroupIndex = 0
2576var mailtoRegexp = regexp.MustCompile(`(?:(?:[\w-_.]+)@(?:[\w-]+(?:\.[\w-]+)+))\b`)
2577
2578func init() {
2579	for index, name := range linkRegexp.SubexpNames() {
2580		if name == "relaxed" {
2581			linkRelaxedGroupIndex = index + 1
2582		}
2583		if name == "strict" {
2584			linkStrictGroupIndex = index + 1
2585		}
2586	}
2587}
2588
2589func DecorateWithLinks(ctx context.Context, body string) string {
2590	var added int
2591	offset := 0
2592	origBody := body
2593
2594	// early out of here if there is no dot
2595	if !(strings.Contains(body, ".") || strings.Contains(body, "://")) {
2596		return body
2597	}
2598	shouldSkipLink := func(body string) bool {
2599		if strings.Contains(strings.Split(body, "/")[0], "@") {
2600			return true
2601		}
2602		for _, scheme := range xurls.SchemesNoAuthority {
2603			if strings.HasPrefix(body, scheme) {
2604				return true
2605			}
2606		}
2607		if strings.HasPrefix(body, "ftp://") || strings.HasPrefix(body, "gopher://") {
2608			return true
2609		}
2610		return false
2611	}
2612	allMatches := linkRegexp.FindAllStringSubmatchIndex(ReplaceQuotedSubstrings(body, true), -1)
2613	for _, match := range allMatches {
2614		var lowhit, highhit int
2615		if len(match) >= linkRelaxedGroupIndex*2 && match[linkRelaxedGroupIndex*2-2] >= 0 {
2616			lowhit = linkRelaxedGroupIndex*2 - 2
2617			highhit = linkRelaxedGroupIndex*2 - 1
2618		} else if len(match) >= linkStrictGroupIndex*2 && match[linkStrictGroupIndex*2-2] >= 0 {
2619			lowhit = linkStrictGroupIndex*2 - 2
2620			highhit = linkStrictGroupIndex*2 - 1
2621		} else {
2622			continue
2623		}
2624
2625		bodyMatch := origBody[match[lowhit]:match[highhit]]
2626		url := bodyMatch
2627		var punycode string
2628		if shouldSkipLink(bodyMatch) {
2629			continue
2630		}
2631		if encoded, err := idna.ToASCII(url); err == nil && encoded != url {
2632			punycode = encoded
2633		}
2634		body, added = DecorateBody(ctx, body, match[lowhit]+offset, match[highhit]-match[lowhit],
2635			chat1.NewUITextDecorationWithLink(chat1.UILinkDecoration{
2636				Url:      bodyMatch,
2637				Punycode: punycode,
2638			}))
2639		offset += added
2640	}
2641
2642	offset = 0
2643	origBody = body
2644	allMatches = mailtoRegexp.FindAllStringIndex(ReplaceQuotedSubstrings(body, true), -1)
2645	for _, match := range allMatches {
2646		if len(match) < 2 {
2647			continue
2648		}
2649		bodyMatch := origBody[match[0]:match[1]]
2650		body, added = DecorateBody(ctx, body, match[0]+offset, match[1]-match[0],
2651			chat1.NewUITextDecorationWithMailto(chat1.UILinkDecoration{
2652				Url: bodyMatch,
2653			}))
2654		offset += added
2655	}
2656
2657	return body
2658}
2659
2660func DecorateWithMentions(ctx context.Context, body string, atMentions []string,
2661	maybeMentions []chat1.MaybeMention, chanMention chat1.ChannelMention,
2662	channelNameMentions []chat1.ChannelNameMention) string {
2663	var added int
2664	offset := 0
2665	if len(atMentions) > 0 || len(maybeMentions) > 0 || chanMention != chat1.ChannelMention_NONE {
2666		atMap := make(map[string]bool)
2667		for _, at := range atMentions {
2668			atMap[at] = true
2669		}
2670		maybeMap := make(map[string]chat1.MaybeMention)
2671		for _, tm := range maybeMentions {
2672			name := tm.Name
2673			if len(tm.Channel) > 0 {
2674				name += "#" + tm.Channel
2675			}
2676			maybeMap[name] = tm
2677		}
2678		inputBody := body
2679		atMatches := parseRegexpNames(ctx, inputBody, atMentionRegExp)
2680		for _, m := range atMatches {
2681			switch {
2682			case m.normalizedName == "here":
2683				fallthrough
2684			case m.normalizedName == "channel":
2685				fallthrough
2686			case m.normalizedName == "everyone":
2687				if chanMention == chat1.ChannelMention_NONE {
2688					continue
2689				}
2690				fallthrough
2691			case atMap[m.normalizedName]:
2692				body, added = DecorateBody(ctx, body, m.position[0]+offset-1, m.Len()+1,
2693					chat1.NewUITextDecorationWithAtmention(m.name))
2694				offset += added
2695			}
2696			if tm, ok := maybeMap[m.name]; ok {
2697				body, added = DecorateBody(ctx, body, m.position[0]+offset-1, m.Len()+1,
2698					chat1.NewUITextDecorationWithMaybemention(tm))
2699				offset += added
2700			}
2701		}
2702	}
2703	if len(channelNameMentions) > 0 {
2704		chanMap := make(map[string]chat1.ConversationID)
2705		for _, c := range channelNameMentions {
2706			chanMap[c.TopicName] = c.ConvID
2707		}
2708		offset = 0
2709		inputBody := body
2710		chanMatches := parseRegexpNames(ctx, inputBody, chanNameMentionRegExp)
2711		for _, c := range chanMatches {
2712			convID, ok := chanMap[c.name]
2713			if !ok {
2714				continue
2715			}
2716			body, added = DecorateBody(ctx, body, c.position[0]+offset-1, c.Len()+1,
2717				chat1.NewUITextDecorationWithChannelnamemention(chat1.UIChannelNameMention{
2718					Name:   c.name,
2719					ConvID: convID.ConvIDStr(),
2720				}))
2721			offset += added
2722		}
2723	}
2724	return body
2725}
2726
2727func EscapeShrugs(ctx context.Context, body string) string {
2728	return strings.Replace(body, `¯\_(ツ)_/¯`, `¯\\\_(ツ)_/¯`, -1)
2729}
2730
2731var startQuote = ">"
2732var newline = []rune("\n")
2733
2734var blockQuoteRegex = regexp.MustCompile("((?s)```.*?```)")
2735var quoteRegex = regexp.MustCompile("((?s)`.*?`)")
2736
2737func ReplaceQuotedSubstrings(xs string, skipAngleQuotes bool) string {
2738	replacer := func(s string) string {
2739		return strings.Repeat("$", len(s))
2740	}
2741	xs = blockQuoteRegex.ReplaceAllStringFunc(xs, replacer)
2742	xs = quoteRegex.ReplaceAllStringFunc(xs, replacer)
2743
2744	// Remove all quoted lines. Because we removed all codeblocks
2745	// before, we only need to consider single lines.
2746	var ret []string
2747	for _, line := range strings.Split(xs, string(newline)) {
2748		if skipAngleQuotes || !strings.HasPrefix(strings.TrimLeft(line, " "), startQuote) {
2749			ret = append(ret, line)
2750		} else {
2751			ret = append(ret, replacer(line))
2752		}
2753	}
2754	return strings.Join(ret, string(newline))
2755}
2756
2757var ErrGetUnverifiedConvNotFound = errors.New("GetUnverifiedConv: conversation not found")
2758var ErrGetVerifiedConvNotFound = errors.New("GetVerifiedConv: conversation not found")
2759
2760func GetUnverifiedConv(ctx context.Context, g *globals.Context, uid gregor1.UID,
2761	convID chat1.ConversationID, dataSource types.InboxSourceDataSourceTyp) (res types.RemoteConversation, err error) {
2762
2763	inbox, err := g.InboxSource.ReadUnverified(ctx, uid, dataSource, &chat1.GetInboxQuery{
2764		ConvIDs:      []chat1.ConversationID{convID},
2765		MemberStatus: chat1.AllConversationMemberStatuses(),
2766	})
2767	if err != nil {
2768		return res, err
2769	}
2770	if len(inbox.ConvsUnverified) == 0 {
2771		return res, ErrGetUnverifiedConvNotFound
2772	}
2773	if !inbox.ConvsUnverified[0].GetConvID().Eq(convID) {
2774		return res, fmt.Errorf("GetUnverifiedConv: convID mismatch: %s != %s",
2775			inbox.ConvsUnverified[0].ConvIDStr, convID)
2776	}
2777	return inbox.ConvsUnverified[0], nil
2778}
2779
2780func FormatConversationName(info chat1.ConversationInfoLocal, myUsername string) string {
2781	switch info.TeamType {
2782	case chat1.TeamType_COMPLEX:
2783		if len(info.TlfName) > 0 && len(info.TopicName) > 0 {
2784			return fmt.Sprintf("%s#%s", info.TlfName, info.TopicName)
2785		}
2786		return info.TlfName
2787	case chat1.TeamType_SIMPLE:
2788		return info.TlfName
2789	case chat1.TeamType_NONE:
2790		users := info.Participants
2791		if len(users) == 1 {
2792			return ""
2793		}
2794		var usersWithoutYou []string
2795		for _, user := range users {
2796			if user.Username != myUsername && user.InConvName {
2797				usersWithoutYou = append(usersWithoutYou, user.Username)
2798			}
2799		}
2800		return strings.Join(usersWithoutYou, ",")
2801	default:
2802		return ""
2803	}
2804}
2805
2806func GetVerifiedConv(ctx context.Context, g *globals.Context, uid gregor1.UID,
2807	convID chat1.ConversationID, dataSource types.InboxSourceDataSourceTyp) (res chat1.ConversationLocal, err error) {
2808	// in case we are being called from within some cancelable context, remove
2809	// it for the purposes of this call, since whatever this is is likely a
2810	// side effect we don't want to get stuck
2811	ctx = globals.CtxRemoveLocalizerCancelable(ctx)
2812	inbox, _, err := g.InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking, dataSource, nil,
2813		&chat1.GetInboxLocalQuery{
2814			ConvIDs:      []chat1.ConversationID{convID},
2815			MemberStatus: chat1.AllConversationMemberStatuses(),
2816		})
2817	if err != nil {
2818		return res, err
2819	}
2820	if len(inbox.Convs) == 0 {
2821		return res, ErrGetVerifiedConvNotFound
2822	}
2823	if !inbox.Convs[0].GetConvID().Eq(convID) {
2824		return res, fmt.Errorf("GetVerifiedConv: convID mismatch: %s != %s",
2825			inbox.Convs[0].GetConvID(), convID)
2826	}
2827	return inbox.Convs[0], nil
2828}
2829
2830func IsMapUnfurl(msg chat1.MessageUnboxed) bool {
2831	if !msg.IsValid() {
2832		return false
2833	}
2834	body := msg.Valid().MessageBody
2835	if !body.IsType(chat1.MessageType_UNFURL) {
2836		return false
2837	}
2838	unfurl := body.Unfurl()
2839	typ, err := unfurl.Unfurl.Unfurl.UnfurlType()
2840	if err != nil {
2841		return false
2842	}
2843	if typ != chat1.UnfurlType_GENERIC {
2844		return false
2845	}
2846	return body.Unfurl().Unfurl.Unfurl.Generic().MapInfo != nil
2847}
2848
2849func DedupStringLists(lists ...[]string) (res []string) {
2850	seen := make(map[string]struct{})
2851	for _, list := range lists {
2852		for _, x := range list {
2853			if _, ok := seen[x]; !ok {
2854				seen[x] = struct{}{}
2855				res = append(res, x)
2856			}
2857		}
2858	}
2859	return res
2860}
2861
2862func DBConvLess(a pager.InboxEntry, b pager.InboxEntry) bool {
2863	if a.GetMtime() > b.GetMtime() {
2864		return true
2865	} else if a.GetMtime() < b.GetMtime() {
2866		return false
2867	}
2868	return !(a.GetConvID().Eq(b.GetConvID()) || a.GetConvID().Less(b.GetConvID()))
2869}
2870
2871func ExportToSummary(i chat1.InboxUIItem) (s chat1.ConvSummary) {
2872	s.Id = i.ConvID
2873	s.IsDefaultConv = i.IsDefaultConv
2874	s.Unread = i.ReadMsgID < i.MaxVisibleMsgID
2875	s.ActiveAt = i.Time.UnixSeconds()
2876	s.ActiveAtMs = i.Time.UnixMilliseconds()
2877	s.FinalizeInfo = i.FinalizeInfo
2878	s.CreatorInfo = i.CreatorInfo
2879	s.MemberStatus = strings.ToLower(i.MemberStatus.String())
2880	s.Supersedes = make([]string, 0, len(i.Supersedes))
2881	for _, super := range i.Supersedes {
2882		s.Supersedes = append(s.Supersedes,
2883			super.ConversationID.String())
2884	}
2885	s.SupersededBy = make([]string, 0, len(i.SupersededBy))
2886	for _, super := range i.SupersededBy {
2887		s.SupersededBy = append(s.SupersededBy,
2888			super.ConversationID.String())
2889	}
2890	switch i.MembersType {
2891	case chat1.ConversationMembersType_IMPTEAMUPGRADE, chat1.ConversationMembersType_IMPTEAMNATIVE:
2892		s.ResetUsers = i.ResetParticipants
2893	}
2894	s.Channel = chat1.ChatChannel{
2895		Name:        i.Name,
2896		Public:      i.IsPublic,
2897		TopicType:   strings.ToLower(i.TopicType.String()),
2898		MembersType: strings.ToLower(i.MembersType.String()),
2899		TopicName:   i.Channel,
2900	}
2901	return s
2902}
2903
2904func supersedersNotEmpty(ctx context.Context, superseders []chat1.ConversationMetadata, convs []types.RemoteConversation) bool {
2905	for _, superseder := range superseders {
2906		for _, conv := range convs {
2907			if superseder.ConversationID.Eq(conv.GetConvID()) {
2908				for _, msg := range conv.Conv.MaxMsgSummaries {
2909					if IsNonEmptyConvMessageType(msg.GetMessageType()) {
2910						return true
2911					}
2912				}
2913			}
2914		}
2915	}
2916	return false
2917}
2918
2919var defaultMemberStatusFilter = []chat1.ConversationMemberStatus{
2920	chat1.ConversationMemberStatus_ACTIVE,
2921	chat1.ConversationMemberStatus_PREVIEW,
2922	chat1.ConversationMemberStatus_RESET,
2923}
2924
2925var defaultExistences = []chat1.ConversationExistence{
2926	chat1.ConversationExistence_ACTIVE,
2927}
2928
2929func ApplyInboxQuery(ctx context.Context, debugLabeler DebugLabeler, query *chat1.GetInboxQuery, rcs []types.RemoteConversation) (res []types.RemoteConversation) {
2930	if query == nil {
2931		query = &chat1.GetInboxQuery{}
2932	}
2933
2934	var queryConvIDMap map[chat1.ConvIDStr]bool
2935	if query.ConvID != nil {
2936		query.ConvIDs = append(query.ConvIDs, *query.ConvID)
2937	}
2938	if len(query.ConvIDs) > 0 {
2939		queryConvIDMap = make(map[chat1.ConvIDStr]bool, len(query.ConvIDs))
2940		for _, c := range query.ConvIDs {
2941			queryConvIDMap[c.ConvIDStr()] = true
2942		}
2943	}
2944
2945	memberStatus := query.MemberStatus
2946	if len(memberStatus) == 0 {
2947		memberStatus = defaultMemberStatusFilter
2948	}
2949	queryMemberStatusMap := map[chat1.ConversationMemberStatus]bool{}
2950	for _, memberStatus := range memberStatus {
2951		queryMemberStatusMap[memberStatus] = true
2952	}
2953
2954	queryStatusMap := map[chat1.ConversationStatus]bool{}
2955	for _, status := range query.Status {
2956		queryStatusMap[status] = true
2957	}
2958
2959	existences := query.Existences
2960	if len(existences) == 0 {
2961		existences = defaultExistences
2962	}
2963	existenceMap := map[chat1.ConversationExistence]bool{}
2964	for _, status := range existences {
2965		existenceMap[status] = true
2966	}
2967
2968	for _, rc := range rcs {
2969		conv := rc.Conv
2970		// Existence check
2971		if _, ok := existenceMap[conv.Metadata.Existence]; !ok && len(existenceMap) > 0 {
2972			continue
2973		}
2974		// Member status check
2975		if _, ok := queryMemberStatusMap[conv.ReaderInfo.Status]; !ok && len(memberStatus) > 0 {
2976			continue
2977		}
2978		// Status check
2979		if _, ok := queryStatusMap[conv.Metadata.Status]; !ok && len(query.Status) > 0 {
2980			continue
2981		}
2982		// Basic checks
2983		if queryConvIDMap != nil && !queryConvIDMap[rc.ConvIDStr] {
2984			continue
2985		}
2986		if query.After != nil && !conv.ReaderInfo.Mtime.After(*query.After) {
2987			continue
2988		}
2989		if query.Before != nil && !conv.ReaderInfo.Mtime.Before(*query.Before) {
2990			continue
2991		}
2992		if query.TopicType != nil && *query.TopicType != conv.Metadata.IdTriple.TopicType {
2993			continue
2994		}
2995		if query.TlfVisibility != nil && *query.TlfVisibility != keybase1.TLFVisibility_ANY &&
2996			*query.TlfVisibility != conv.Metadata.Visibility {
2997			continue
2998		}
2999		if query.UnreadOnly && !conv.IsUnread() {
3000			continue
3001		}
3002		if query.ReadOnly && conv.IsUnread() {
3003			continue
3004		}
3005		if query.TlfID != nil && !query.TlfID.Eq(conv.Metadata.IdTriple.Tlfid) {
3006			continue
3007		}
3008		if query.TopicName != nil && rc.LocalMetadata != nil &&
3009			*query.TopicName != rc.LocalMetadata.TopicName {
3010			continue
3011		}
3012		// If we are finalized and are superseded, then don't return this
3013		if query.OneChatTypePerTLF == nil ||
3014			(query.OneChatTypePerTLF != nil && *query.OneChatTypePerTLF) {
3015			if conv.Metadata.FinalizeInfo != nil && len(conv.Metadata.SupersededBy) > 0 && len(query.ConvIDs) == 0 {
3016				if supersedersNotEmpty(ctx, conv.Metadata.SupersededBy, rcs) {
3017					continue
3018				}
3019			}
3020		}
3021		res = append(res, rc)
3022	}
3023	filtered := len(rcs) - len(res)
3024	debugLabeler.Debug(ctx, "applyQuery: query: %+v, res size: %d filtered: %d", query, len(res), filtered)
3025	return res
3026}
3027
3028func ToLastActiveStatus(mtime gregor1.Time) chat1.LastActiveStatus {
3029	lastActive := int(time.Since(mtime.Time()).Round(time.Hour).Hours())
3030	switch {
3031	case lastActive <= 24: // 1 day
3032		return chat1.LastActiveStatus_ACTIVE
3033	case lastActive <= 24*7: // 7 days
3034		return chat1.LastActiveStatus_RECENTLY_ACTIVE
3035	default:
3036		return chat1.LastActiveStatus_NONE
3037	}
3038}
3039
3040func GetConvParticipantUsernames(ctx context.Context, g *globals.Context, uid gregor1.UID,
3041	convID chat1.ConversationID) (parts []string, err error) {
3042	uids, err := g.ParticipantsSource.Get(ctx, uid, convID, types.InboxSourceDataSourceAll)
3043	if err != nil {
3044		return parts, err
3045	}
3046	kuids := make([]keybase1.UID, 0, len(uids))
3047	for _, uid := range uids {
3048		kuids = append(kuids, keybase1.UID(uid.String()))
3049	}
3050	rows, err := g.UIDMapper.MapUIDsToUsernamePackages(ctx, g, kuids, 0, 0, false)
3051	if err != nil {
3052		return parts, err
3053	}
3054	parts = make([]string, 0, len(rows))
3055	for _, row := range rows {
3056		parts = append(parts, row.NormalizedUsername.String())
3057	}
3058	return parts, nil
3059}
3060
3061func IsDeletedConvError(err error) bool {
3062	switch err.(type) {
3063	case libkb.ChatBadConversationError,
3064		libkb.ChatNotInTeamError,
3065		libkb.ChatNotInConvError:
3066		return true
3067	default:
3068		return false
3069	}
3070}
3071