From 7097e7882768fe5c25ad8e8aa8624d7883a3f983 Mon Sep 17 00:00:00 2001 From: Joshua Blum Date: Mon, 16 Mar 2026 14:29:03 -0400 Subject: [PATCH 1/3] Copy edit exploding error messages, fix snippet for exploders --- go/chat/localizer.go | 5 +++-- go/chat/utils/utils.go | 13 +++++++++++-- go/chat/utils/utils_test.go | 30 ++++++++++++++++++++++++++++++ go/ephemeral/common_test.go | 2 +- go/ephemeral/errors.go | 6 +++--- 5 files changed, 48 insertions(+), 8 deletions(-) diff --git a/go/chat/localizer.go b/go/chat/localizer.go index af3aa42fb48b..444624b75473 100644 --- a/go/chat/localizer.go +++ b/go/chat/localizer.go @@ -845,8 +845,9 @@ func (s *localizerPipeline) localizeConversation(ctx context.Context, uid gregor var maxValidID chat1.MessageID s.Debug(ctx, "localizing %d max msgs", len(maxMsgs)) for _, mm := range maxMsgs { - if mm.IsValid() && - utils.IsSnippetChatMessageType(mm.GetMessageType()) && + isValidSnippet := mm.IsValid() && utils.IsSnippetChatMessageType(mm.GetMessageType()) + isEphemeralErr := mm.IsError() && mm.Error().IsEphemeral + if (isValidSnippet || isEphemeralErr) && (conversationLocal.Info.SnippetMsg == nil || conversationLocal.Info.SnippetMsg.GetMessageID() < mm.GetMessageID()) { conversationLocal.Info.SnippetMsg = new(chat1.MessageUnboxed) diff --git a/go/chat/utils/utils.go b/go/chat/utils/utils.go index e65d478aa612..9b4442e618b5 100644 --- a/go/chat/utils/utils.go +++ b/go/chat/utils/utils.go @@ -1116,10 +1116,13 @@ func formatDuration(dur time.Duration) string { func getMsgSnippetDecoration(msg chat1.MessageUnboxed) chat1.SnippetDecoration { var msgBody chat1.MessageBody - if msg.IsValid() { + switch { + case msg.IsValid(): msgBody = msg.Valid().MessageBody - } else { + case msg.IsOutbox(): msgBody = msg.Outbox().Msg.MessageBody + default: + return chat1.SnippetDecoration_NONE } switch msg.GetMessageType() { case chat1.MessageType_ATTACHMENT: @@ -1221,6 +1224,12 @@ func GetMsgSnippet(ctx context.Context, g *globals.Context, uid gregor1.UID, msg conv chat1.ConversationLocal, currentUsername string, ) (decoration chat1.SnippetDecoration, snippet string, snippetDecorated string) { if !msg.IsValid() && !msg.IsOutbox() { + if msg.IsError() && msg.Error().IsEphemeral { + if msg.Error().IsEphemeralExpired(time.Now()) { + return chat1.SnippetDecoration_EXPLODED_MESSAGE, "Message exploded.", "" + } + return chat1.SnippetDecoration_EXPLODING_MESSAGE, msg.Error().ErrMsg, "" + } return chat1.SnippetDecoration_NONE, "", "" } defer func() { diff --git a/go/chat/utils/utils_test.go b/go/chat/utils/utils_test.go index a8b0acdabc0c..6ef13a41f6e5 100644 --- a/go/chat/utils/utils_test.go +++ b/go/chat/utils/utils_test.go @@ -1086,6 +1086,36 @@ func TestSearchableRemoteConversationName(t *testing.T) { searchableRemoteConversationNameFromStr("joshblum,zoommikem,mikem,zoomua,mikem", "mikem")) } +func TestGetMsgSnippetEphemeralError(t *testing.T) { + ctx := context.Background() + conv := chat1.ConversationLocal{} + errMsg := "This exploding message is not available because this device was created after it was sent" + + // Non-expired ephemeral error: should surface the error message with EXPLODING_MESSAGE decoration. + msg := chat1.NewMessageUnboxedWithError(chat1.MessageUnboxedError{ + ErrType: chat1.MessageUnboxedErrorType_EPHEMERAL, + ErrMsg: errMsg, + IsEphemeral: true, + Etime: gregor1.ToTime(time.Now().Add(time.Hour)), + MessageType: chat1.MessageType_TEXT, + }) + decoration, snippet, _ := GetMsgSnippet(ctx, nil, gregor1.UID{}, msg, conv, "alice") + require.Equal(t, chat1.SnippetDecoration_EXPLODING_MESSAGE, decoration) + require.Equal(t, errMsg, snippet) + + // Expired ephemeral error: should show "Message exploded." with EXPLODED_MESSAGE decoration. + msg = chat1.NewMessageUnboxedWithError(chat1.MessageUnboxedError{ + ErrType: chat1.MessageUnboxedErrorType_EPHEMERAL, + ErrMsg: errMsg, + IsEphemeral: true, + Etime: gregor1.ToTime(time.Now().Add(-time.Hour)), + MessageType: chat1.MessageType_TEXT, + }) + decoration, snippet, _ = GetMsgSnippet(ctx, nil, gregor1.UID{}, msg, conv, "alice") + require.Equal(t, chat1.SnippetDecoration_EXPLODED_MESSAGE, decoration) + require.Equal(t, "Message exploded.", snippet) +} + func TestStripUsernameFromConvName(t *testing.T) { // Only the username as a complete segment is removed; "mikem" inside "zoommikem" must not be stripped require.Equal(t, "joshblum,zoommikem,zoomua", diff --git a/go/ephemeral/common_test.go b/go/ephemeral/common_test.go index dd7473975259..e1e88d6cfbc6 100644 --- a/go/ephemeral/common_test.go +++ b/go/ephemeral/common_test.go @@ -132,7 +132,7 @@ func TestEphemeralPluralization(t *testing.T) { require.Equal(t, humanMsg, pluralized) pluralized = PluralizeErrorMessage(humanMsg, 2) - require.Equal(t, "2 exploding messages are not available, because this device was created after it was sent", pluralized) + require.Equal(t, "2 exploding messages are not available because this device was created after it was sent", pluralized) pluralized = PluralizeErrorMessage(DefaultHumanErrMsg, 2) require.Equal(t, "2 exploding messages are not available", pluralized) diff --git a/go/ephemeral/errors.go b/go/ephemeral/errors.go index 29f0b1ebd009..9ddef08e72b1 100644 --- a/go/ephemeral/errors.go +++ b/go/ephemeral/errors.go @@ -82,8 +82,8 @@ func newTransientEphemeralKeyError(err EphemeralKeyError) EphemeralKeyError { const ( DefaultHumanErrMsg = "This exploding message is not available" DefaultPluralHumanErrMsg = "%d exploding messages are not available" - DeviceCloneErrMsg = "cloned devices do not support exploding messages" - DeviceCloneWithOneshotErrMsg = "to support exploding messages in `oneshot` mode, you need a separate paper key for each running instance" + DeviceCloneErrMsg = "because this device has been cloned" + DeviceCloneWithOneshotErrMsg = "because this device is running in `oneshot` mode; to support exploding messages in `oneshot` mode, you need a separate paper key for each running instance" DeviceAfterEKErrMsg = "because this device was created after it was sent" MemberAfterEKErrMsg = "because you joined the team after it was sent" DeviceStaleErrMsg = "because this device wasn't online to generate an exploding key" @@ -159,7 +159,7 @@ func humanMsgWithPrefix(humanMsg string) string { if humanMsg == "" { humanMsg = DefaultHumanErrMsg } else if !strings.Contains(humanMsg, DefaultHumanErrMsg) { - humanMsg = fmt.Sprintf("%s, %s", DefaultHumanErrMsg, humanMsg) + humanMsg = fmt.Sprintf("%s %s", DefaultHumanErrMsg, humanMsg) } return humanMsg } From 3a22d99e4fc2a55690c24b8267c5b79182dad6c6 Mon Sep 17 00:00:00 2001 From: Joshua Blum Date: Mon, 16 Mar 2026 14:52:50 -0400 Subject: [PATCH 2/3] x --- go/chat/utils/utils_test.go | 2 +- go/ephemeral/common_test.go | 5 ++++- go/ephemeral/errors.go | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/go/chat/utils/utils_test.go b/go/chat/utils/utils_test.go index 6ef13a41f6e5..e39b1ac63060 100644 --- a/go/chat/utils/utils_test.go +++ b/go/chat/utils/utils_test.go @@ -1089,7 +1089,7 @@ func TestSearchableRemoteConversationName(t *testing.T) { func TestGetMsgSnippetEphemeralError(t *testing.T) { ctx := context.Background() conv := chat1.ConversationLocal{} - errMsg := "This exploding message is not available because this device was created after it was sent" + errMsg := "This exploding message is not available because this device was created after the message was sent" // Non-expired ephemeral error: should surface the error message with EXPLODING_MESSAGE decoration. msg := chat1.NewMessageUnboxedWithError(chat1.MessageUnboxedError{ diff --git a/go/ephemeral/common_test.go b/go/ephemeral/common_test.go index e1e88d6cfbc6..eb2eefdb72be 100644 --- a/go/ephemeral/common_test.go +++ b/go/ephemeral/common_test.go @@ -132,7 +132,10 @@ func TestEphemeralPluralization(t *testing.T) { require.Equal(t, humanMsg, pluralized) pluralized = PluralizeErrorMessage(humanMsg, 2) - require.Equal(t, "2 exploding messages are not available because this device was created after it was sent", pluralized) + require.Equal(t, "2 exploding messages are not available because this device was created after the messages were sent", pluralized) + + pluralized = PluralizeErrorMessage(humanMsgWithPrefix(MemberAfterEKErrMsg), 3) + require.Equal(t, "3 exploding messages are not available because you joined the team after the messages were sent", pluralized) pluralized = PluralizeErrorMessage(DefaultHumanErrMsg, 2) require.Equal(t, "2 exploding messages are not available", pluralized) diff --git a/go/ephemeral/errors.go b/go/ephemeral/errors.go index 9ddef08e72b1..5bab64893c2b 100644 --- a/go/ephemeral/errors.go +++ b/go/ephemeral/errors.go @@ -84,8 +84,8 @@ const ( DefaultPluralHumanErrMsg = "%d exploding messages are not available" DeviceCloneErrMsg = "because this device has been cloned" DeviceCloneWithOneshotErrMsg = "because this device is running in `oneshot` mode; to support exploding messages in `oneshot` mode, you need a separate paper key for each running instance" - DeviceAfterEKErrMsg = "because this device was created after it was sent" - MemberAfterEKErrMsg = "because you joined the team after it was sent" + DeviceAfterEKErrMsg = "because this device was created after the message was sent" + MemberAfterEKErrMsg = "because you joined the team after the message was sent" DeviceStaleErrMsg = "because this device wasn't online to generate an exploding key" UserStaleErrMsg = "because you weren't online to generate new exploding keys" ) From aafd85fc4c3d5e328eab07ee7484b34deb6cc8a4 Mon Sep 17 00:00:00 2001 From: Joshua Blum Date: Mon, 16 Mar 2026 15:06:50 -0400 Subject: [PATCH 3/3] x --- go/chat/utils/utils.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/go/chat/utils/utils.go b/go/chat/utils/utils.go index 9b4442e618b5..36e1042b8723 100644 --- a/go/chat/utils/utils.go +++ b/go/chat/utils/utils.go @@ -1223,6 +1223,11 @@ func GetMsgSnippetBody(ctx context.Context, g *globals.Context, uid gregor1.UID, func GetMsgSnippet(ctx context.Context, g *globals.Context, uid gregor1.UID, msg chat1.MessageUnboxed, conv chat1.ConversationLocal, currentUsername string, ) (decoration chat1.SnippetDecoration, snippet string, snippetDecorated string) { + defer func() { + if len(snippetDecorated) == 0 { + snippetDecorated = snippet + } + }() if !msg.IsValid() && !msg.IsOutbox() { if msg.IsError() && msg.Error().IsEphemeral { if msg.Error().IsEphemeralExpired(time.Now()) { @@ -1232,11 +1237,6 @@ func GetMsgSnippet(ctx context.Context, g *globals.Context, uid gregor1.UID, msg } return chat1.SnippetDecoration_NONE, "", "" } - defer func() { - if len(snippetDecorated) == 0 { - snippetDecorated = snippet - } - }() var senderUsername string if msg.IsValid() {