@bobfrankston/rmfmail 1.1.102 → 1.1.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2693,7 +2693,7 @@ export class MailxDB {
2693
2693
  * Multi-name-per-email is supported: the same email can carry distinct
2694
2694
  * (source, name) rows — Bob's wife and Bob Smith both at bob@example.com
2695
2695
  * surface as two rows, each typing-completable by their own name. */
2696
- searchContacts(query: string, limit = 10): { name: string; email: string; source: string; useCount: number }[] {
2696
+ searchContacts(query: string, limit = 10): { name: string; email: string; source: string; sources: string[]; useCount: number }[] {
2697
2697
  query = (query || "").trim();
2698
2698
  if (!query) return [];
2699
2699
  // Split into whitespace-separated tokens. Each token must appear in
@@ -2753,21 +2753,33 @@ export class MailxDB {
2753
2753
  rows.sort((a, b) => score(b) - score(a));
2754
2754
  // Dedup by lowercased email — same address often appears as both
2755
2755
  // source='google' (synced from Google Contacts) and source='discovered'
2756
- // (auto-collected from sent mail). The user only wants to see the
2757
- // best entry; we keep the higher-ranked source (which the sort above
2758
- // has already put first), and silently drop the duplicate. Without
2759
- // this, the autocomplete dropdown showed two identical Kevin Healy
2760
- // rows just labeled GOOGLE and DISCOVERED.
2761
- const seenEmails = new Set<string>();
2762
- rows = rows.filter(r => {
2756
+ // (auto-collected from sent mail). Keep the higher-ranked row (already
2757
+ // sorted first) and FOLD subsequent rows' sources into a `sources` array
2758
+ // on the survivor so the dropdown can display "google, discovered"
2759
+ // instead of showing two duplicate lines.
2760
+ const merged = new Map<string, any>();
2761
+ for (const r of rows) {
2763
2762
  const k = (r.email || "").toLowerCase();
2764
- if (!k) return true;
2765
- if (seenEmails.has(k)) return false;
2766
- seenEmails.add(k);
2767
- return true;
2768
- });
2769
- rows = rows.slice(0, limit);
2770
- return rows.map(r => ({ name: r.name, email: r.email, source: r.source, useCount: r.use_count }));
2763
+ if (!k) { merged.set(`__noemail_${merged.size}`, { ...r, _sources: new Set([r.source]) }); continue; }
2764
+ const existing = merged.get(k);
2765
+ if (!existing) {
2766
+ merged.set(k, { ...r, _sources: new Set([r.source]) });
2767
+ } else {
2768
+ existing._sources.add(r.source);
2769
+ // If this row has a non-empty name and the kept one didn't,
2770
+ // promote the name — Google rows often have proper names while
2771
+ // discovered rows are bare-email.
2772
+ if (!existing.name && r.name) existing.name = r.name;
2773
+ }
2774
+ }
2775
+ const out = Array.from(merged.values()).slice(0, limit);
2776
+ return out.map(r => ({
2777
+ name: r.name,
2778
+ email: r.email,
2779
+ source: r.source,
2780
+ sources: Array.from(r._sources as Set<string>).filter(Boolean),
2781
+ useCount: r.use_count,
2782
+ }));
2771
2783
  }
2772
2784
 
2773
2785
  /** List all contacts (address-book view) with pagination + optional filter. */