fix(mail): 답장도 리스트에 각각 표시(스레드 단위 가시성) + 동기화 진행 신호
All checks were successful
build-and-push / build (push) Successful in 32s

- 가시성을 메일 단위 → '내가 참여한 스레드 전체'로: 한 스레드에서 한 통이라도
  from/to/cc면 그 스레드의 원문·답장 모두 노출(각각의 행). 무관 스레드는 숨김.
- isSyncing(): 백필/수동 동기화 진행 중이면 mails 응답에 syncing=true → 프론트 폴링

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
theorose49 2026-06-30 14:01:33 +09:00
parent 7e1bb6606e
commit 14e8a62f76
2 changed files with 25 additions and 2 deletions

View File

@ -603,6 +603,9 @@ func (s *Server) handleListProjectMails(w http.ResponseWriter, r *http.Request)
if st.LastError != "" {
resp["error"] = st.LastError
}
if isSyncing(id) {
resp["syncing"] = true
}
// First ever view → kick off full backfill in the background (only if mail
// integration is configured). Already-stored mail is shown regardless.
if !synced && s.mail.Enabled() {
@ -619,10 +622,24 @@ func (s *Server) handleListProjectMails(w http.ResponseWriter, r *http.Request)
notes[n.MessageID] = n
}
me := s.email(r)
// 가시성: 내가 참여한 "스레드 전체"를 노출(원문 + 답장 각각). 한 스레드에서
// 한 통이라도 내가 from/to/cc면 그 스레드의 모든 메일이 보인다.
myThreads := map[string]bool{}
involvedSolo := map[string]bool{}
for _, row := range rows {
m := mailsync.Message{From: row.FromAddr, To: row.ToAddr, Cc: row.CcAddr}
if m.Involves(me) {
if row.ThreadID != "" {
myThreads[row.ThreadID] = true
} else {
involvedSolo[row.MessageID] = true
}
}
}
items := make([]mailItem, 0, len(rows))
for _, row := range rows {
m := mailsync.Message{ID: row.MessageID, From: row.FromAddr, To: row.ToAddr, Cc: row.CcAddr}
if !m.Involves(me) {
visible := involvedSolo[row.MessageID] || (row.ThreadID != "" && myThreads[row.ThreadID])
if !visible {
continue
}
it := mailItem{ProjectMailMsg: row}

View File

@ -16,6 +16,12 @@ import (
// syncInFlight prevents concurrent syncs of the same project.
var syncInFlight sync.Map // projectID -> struct{}
// isSyncing reports whether a sync is currently running for the project.
func isSyncing(projectID string) bool {
_, ok := syncInFlight.Load(projectID)
return ok
}
// syncProjectMail pulls the project's client-domain mail across ALL active member
// mailboxes (domain-wide delegation), upserts the headers into ProjectMailMsg, and
// records sync state. full=true pages the entire history; otherwise just the newest