webmail: fix js error rerendering additional headers after updated keywords

i've seen the error a few times:

	msgheaderElem.children[(msgheaderElem.children.length - 1)] is undefined

i've seen it happen after sending a reply (with the "answered" flag added).
the updateKeywords callback would render the message again, but the code for
rendering the "additional headers" table rows again was making invalid
assumptions.

the approach is now changed. the backend now just immediately sends the
additional headers to the frontend. before, the frontend would first render the
base message, then render again once the headers came in for the parsed
message. this also prevents a reflow for the (quite common) case that one of
the additional headers are present in the message.
This commit is contained in:
Mechiel Lukkien
2025-01-13 14:53:43 +01:00
parent f7193bd4c3
commit 1e15a10b66
11 changed files with 167 additions and 119 deletions

View File

@ -172,8 +172,9 @@ type MessageItem struct {
Attachments []Attachment
IsSigned bool
IsEncrypted bool
FirstLine string // Of message body, for showing as preview.
MatchQuery bool // If message does not match query, it can still be included because of threading.
FirstLine string // Of message body, for showing as preview.
MatchQuery bool // If message does not match query, it can still be included because of threading.
MoreHeaders [][2]string // All headers from store.Settings.ShowHeaders that are present.
}
// ParsedMessage has more parsed/derived information about a message, intended
@ -488,6 +489,23 @@ type ioErr struct {
err error
}
// ensure we have a non-nil moreHeaders, taking it from Settings.
func ensureMoreHeaders(tx *bstore.Tx, moreHeaders []string) ([]string, error) {
if moreHeaders != nil {
return moreHeaders, nil
}
s := store.Settings{ID: 1}
if err := tx.Get(&s); err != nil {
return nil, fmt.Errorf("get settings: %v", err)
}
moreHeaders = s.ShowHeaders
if moreHeaders == nil {
moreHeaders = []string{} // Ensure we won't get Settings again next call.
}
return moreHeaders, nil
}
// serveEvents serves an SSE connection. Authentication is done through a query
// string parameter "singleUseToken", a one-time-use token returned by the Token
// API call.
@ -824,6 +842,17 @@ func serveEvents(ctx context.Context, log mlog.Log, accountPath string, w http.R
return bstore.QueryTx[store.Message](xtx).FilterEqual("Expunged", false).FilterNonzero(store.Message{MailboxID: mailboxID, UID: uid}).Get()
}
// Additional headers from settings to add to MessageItems.
var moreHeaders []string
xmoreHeaders := func() []string {
err := ensureTx()
xcheckf(ctx, err, "transaction")
moreHeaders, err = ensureMoreHeaders(xtx, moreHeaders)
xcheckf(ctx, err, "ensuring more headers")
return moreHeaders
}
// Return uids that are within range in view. Because the end has been reached, or
// because the UID is not after the last message.
xchangedUIDs := func(mailboxID int64, uids []store.UID, isRemove bool) (changedUIDs []store.UID) {
@ -860,8 +889,9 @@ func serveEvents(ctx context.Context, log mlog.Log, accountPath string, w http.R
if !ok && !thread {
continue
}
state := msgState{acc: acc}
mi, err := messageItem(log, m, &state)
mi, err := messageItem(log, m, &state, xmoreHeaders())
state.clear()
xcheckf(ctx, err, "make messageitem")
mi.MatchQuery = ok
@ -870,7 +900,7 @@ func serveEvents(ctx context.Context, log mlog.Log, accountPath string, w http.R
if !thread && req.Query.Threading != ThreadOff {
err := ensureTx()
xcheckf(ctx, err, "transaction")
more, _, err := gatherThread(log, xtx, acc, v, m, 0, false)
more, _, err := gatherThread(log, xtx, acc, v, m, 0, false, xmoreHeaders())
xcheckf(ctx, err, "gathering thread messages for id %d, thread %d", m.ID, m.ThreadID)
mil = append(mil, more...)
v.threadIDs[m.ThreadID] = struct{}{}
@ -1460,6 +1490,8 @@ func queryMessages(ctx context.Context, log mlog.Log, acc *store.Account, tx *bs
q.FilterFn(wordsFilter)
}
var moreHeaders []string // From store.Settings.ShowHeaders
if query.OrderAsc {
q.SortAsc("Received")
} else {
@ -1501,13 +1533,19 @@ func queryMessages(ctx context.Context, log mlog.Log, acc *store.Account, tx *bs
}
}
mi, err := messageItem(log, m, &state)
var err error
moreHeaders, err = ensureMoreHeaders(tx, moreHeaders)
if err != nil {
return fmt.Errorf("ensuring more headers: %v", err)
}
mi, err := messageItem(log, m, &state, moreHeaders)
if err != nil {
return fmt.Errorf("making messageitem for message %d: %v", m.ID, err)
}
mil := []MessageItem{mi}
if query.Threading != ThreadOff {
more, xpm, err := gatherThread(log, tx, acc, v, m, page.DestMessageID, page.AnchorMessageID == 0 && have == 0)
more, xpm, err := gatherThread(log, tx, acc, v, m, page.DestMessageID, page.AnchorMessageID == 0 && have == 0, moreHeaders)
if err != nil {
return fmt.Errorf("gathering thread messages for id %d, thread %d: %v", m.ID, m.ThreadID, err)
}
@ -1576,7 +1614,7 @@ func queryMessages(ctx context.Context, log mlog.Log, acc *store.Account, tx *bs
}
}
func gatherThread(log mlog.Log, tx *bstore.Tx, acc *store.Account, v view, m store.Message, destMessageID int64, first bool) ([]MessageItem, *ParsedMessage, error) {
func gatherThread(log mlog.Log, tx *bstore.Tx, acc *store.Account, v view, m store.Message, destMessageID int64, first bool, moreHeaders []string) ([]MessageItem, *ParsedMessage, error) {
if m.ThreadID == 0 {
// If we would continue, FilterNonzero would fail because there are no non-zero fields.
return nil, nil, fmt.Errorf("message has threadid 0, account is probably still being upgraded, try turning threading off until the upgrade is done")
@ -1601,7 +1639,7 @@ func gatherThread(log mlog.Log, tx *bstore.Tx, acc *store.Account, v view, m sto
xstate := msgState{acc: acc}
defer xstate.clear()
mi, err := messageItem(log, tm, &xstate)
mi, err := messageItem(log, tm, &xstate, moreHeaders)
if err != nil {
return fmt.Errorf("making messageitem for message %d, for thread %d: %v", tm.ID, m.ThreadID, err)
}