Skip to content

Commit 71f2143

Browse files
Add body contains filter to 'Tag similar messages' modal
The modal now supports three filters: - From (exact, read-only) - Subject contains (optional) - Body contains (optional) Both subject and body use search_text~= substring matching. When both are provided, they narrow results further (AND). Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 77f4abc commit 71f2143

3 files changed

Lines changed: 28 additions & 9 deletions

File tree

bulk.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ type bulkReq struct {
1818
Q string `json:"q"`
1919
AllPages bool `json:"all_pages"`
2020
// Similar-message filter: tag all from same sender, optionally with
21-
// subject containing a substring. Used by the "tag similar" modal.
22-
FromAddr string `json:"from_addr"`
21+
// subject and/or body containing a substring. Used by the "tag similar" modal.
22+
FromAddr string `json:"from_addr"`
2323
SubjectContains string `json:"subject_contains"`
24+
BodyContains string `json:"body_contains"`
2425
}
2526

2627
func handleBulkAPI(w http.ResponseWriter, r *http.Request) {
@@ -46,7 +47,7 @@ func handleBulkAPI(w http.ResponseWriter, r *http.Request) {
4647
var err error
4748
if req.FromAddr != "" {
4849
has, missing := viewLinks(req.View, req.TagView)
49-
ids, err = collectIDsFiltered(p, has, missing, "", req.FromAddr, req.SubjectContains, mbID, isAdmin)
50+
ids, err = collectIDsFiltered(p, has, missing, "", req.FromAddr, req.SubjectContains, req.BodyContains, mbID, isAdmin)
5051
} else {
5152
ids, err = collectIDsLinked(p, req.View, req.TagView, req.Q, mbID, isAdmin)
5253
}
@@ -385,14 +386,15 @@ func filterIDsOwnedByMailbox(p *Poche, ids []string, mbID string) []string {
385386

386387
func collectIDsLinked(p *Poche, view, tagView, q string, mbID string, isAdmin bool) ([]string, error) {
387388
has, missing := viewLinks(view, tagView)
388-
return collectIDsFiltered(p, has, missing, q, "", "", mbID, isAdmin)
389+
return collectIDsFiltered(p, has, missing, q, "", "", "", mbID, isAdmin)
389390
}
390391

391392
// collectIDsFiltered is the generalized collector used by both the
392393
// all_pages bulk path (from view/tagView/q) and the "tag similar" path
393-
// (from from_addr + subject_contains). The where clause is built from
394-
// search text, from_addr, and subject_contains, then mailbox-scoped.
395-
func collectIDsFiltered(p *Poche, has, missing []string, q, fromAddr, subjectContains, mbID string, isAdmin bool) ([]string, error) {
394+
// (from from_addr + subject_contains + body_contains). The where clause
395+
// is built from search text, from_addr, subject_contains, and body_contains,
396+
// then mailbox-scoped.
397+
func collectIDsFiltered(p *Poche, has, missing []string, q, fromAddr, subjectContains, bodyContains, mbID string, isAdmin bool) ([]string, error) {
396398
var clauses []string
397399
if needle := sanitizeQ(q); needle != "" {
398400
clauses = append(clauses, "search_text~="+needle)
@@ -403,6 +405,9 @@ func collectIDsFiltered(p *Poche, has, missing []string, q, fromAddr, subjectCon
403405
if subjectContains != "" {
404406
clauses = append(clauses, "search_text~="+strings.ToLower(subjectContains))
405407
}
408+
if bodyContains != "" {
409+
clauses = append(clauses, "search_text~="+strings.ToLower(bodyContains))
410+
}
406411
if !isAdmin && mbID != "" {
407412
clauses = append(clauses, "mailbox_id="+mbID)
408413
}

ui/js/components.jsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -606,21 +606,23 @@ function TagRow({ name, count, unreadCount, active, navBtn, label, onOpen, onRen
606606
function TagSimilarModal({ open, onClose, fromAddr, tagName, token, onTagged }) {
607607
const { t } = useI18n();
608608
const [subjectFilter, setSubjectFilter] = React.useState("");
609+
const [bodyFilter, setBodyFilter] = React.useState("");
609610
const [matchCount, setMatchCount] = React.useState(null);
610611
const [busy, setBusy] = React.useState(false);
611612

612-
// Fetch count of messages from same sender, not already tagged, with optional subject filter
613+
// Fetch count of messages from same sender, not already tagged, with optional subject/body filter
613614
React.useEffect(() => {
614615
if (!open || !token || !fromAddr) return;
615616
const where = ["from_addr=" + fromAddr];
616617
if (subjectFilter.trim()) where.push("search_text~=" + subjectFilter.trim().toLowerCase());
618+
if (bodyFilter.trim()) where.push("search_text~=" + bodyFilter.trim().toLowerCase());
617619
const params = new URLSearchParams();
618620
params.set("where", where.join(","));
619621
params.append("missing_link", "message_tags.message_id:tag=" + tagName);
620622
apiFetch(token, "/api/messages/count?" + params.toString())
621623
.then((d) => setMatchCount(d.count || 0))
622624
.catch(() => setMatchCount(0));
623-
}, [open, token, fromAddr, tagName, subjectFilter]);
625+
}, [open, token, fromAddr, tagName, subjectFilter, bodyFilter]);
624626

625627
if (!open) return null;
626628

@@ -631,6 +633,7 @@ function TagSimilarModal({ open, onClose, fromAddr, tagName, token, onTagged })
631633
tag: tagName,
632634
from_addr: fromAddr,
633635
subject_contains: subjectFilter.trim() || undefined,
636+
body_contains: bodyFilter.trim() || undefined,
634637
})
635638
.then(() => {
636639
onTagged();
@@ -665,6 +668,15 @@ function TagSimilarModal({ open, onClose, fromAddr, tagName, token, onTagged })
665668
className="w-full bg-paper border border-paper-line rounded px-2 py-1.5 text-sm text-ink focus:outline-none focus:border-accent"
666669
/>
667670
</div>
671+
<div>
672+
<label className="text-xs text-ink-dim block mb-1">{t("tag_similar_body")}</label>
673+
<input
674+
value={bodyFilter}
675+
onChange={(e) => setBodyFilter(e.target.value)}
676+
placeholder=""
677+
className="w-full bg-paper border border-paper-line rounded px-2 py-1.5 text-sm text-ink focus:outline-none focus:border-accent"
678+
/>
679+
</div>
668680
<div className="text-sm text-ink-muted">
669681
{matchCount === null ? "…" : matchCount === 0
670682
? t("tag_similar_no_match")

ui/js/i18n.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ const I18N = {
9494
tag_similar_title: "Tag similar messages",
9595
tag_similar_from: "From",
9696
tag_similar_subject: "Subject contains (optional)",
97+
tag_similar_body: "Body contains (optional)",
9798
tag_similar_count: (n) => n + " matching message" + (n !== 1 ? "s" : ""),
9899
tag_similar_apply: (n) => "Tag " + n + " message" + (n !== 1 ? "s" : ""),
99100
tag_similar_no_match: "No matching messages",
@@ -264,6 +265,7 @@ const I18N = {
264265
tag_similar_title: "Étiqueter les messages similaires",
265266
tag_similar_from: "De",
266267
tag_similar_subject: "Le sujet contient (optionnel)",
268+
tag_similar_body: "Le corps contient (optionnel)",
267269
tag_similar_count: (n) => n + " message" + (n !== 1 ? "s" : "") + " correspondant" + (n !== 1 ? "s" : ""),
268270
tag_similar_apply: (n) => "Étiqueter " + n + " message" + (n !== 1 ? "s" : ""),
269271
tag_similar_no_match: "Aucun message correspondant",

0 commit comments

Comments
 (0)