diff --git a/internal/httpapi/handlers_projects.go b/internal/httpapi/handlers_projects.go index 5e860fe..ea428af 100644 --- a/internal/httpapi/handlers_projects.go +++ b/internal/httpapi/handlers_projects.go @@ -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} diff --git a/internal/httpapi/mail_sync.go b/internal/httpapi/mail_sync.go index 44f45d4..8d2e3f9 100644 --- a/internal/httpapi/mail_sync.go +++ b/internal/httpapi/mail_sync.go @@ -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