Refine email-proposal review UX (v0.1.0:91)
Three post-smoke refinements to the Matrix email-proposal review:
1. Dash separators (bot): every card/reply is framed with a dash rule top and
bottom so threads stop bleeding together vertically on mobile.
2. Remove decided threads (bot): on a conclusive approve/dismiss from either
surface, the bot redacts the card (client.room_redact) so the room clears
down to only undecided items. Redacting the bot's own card needs no power;
the web->Matrix path now redacts instead of posting a closure note.
3. Clearer note wording (server v91 + bot): the proposed grid note now names who
emailed whom -- "{teammate} emailed {investor}" (outbound) / "{sender} emailed
the team" (inbound) -- instead of an ambiguous "Sent"/"Received". Outbound
detection also matches our corporate domain (public providers excluded), so a
teammate's mail from a non-enrolled @ten31.xyz address no longer reads as
"Received". Going-forward only; no schema change. The card drops its bare
direction label since the note now carries the relationship.
Tests updated; 30/30 green, render-smoke green.
This commit is contained in:
+47
-5
@@ -5731,8 +5731,45 @@ def _append_grid_note(conn, inv_id, inv_name, note, updated_by=None):
|
||||
return True
|
||||
|
||||
|
||||
def _activity_note_text(sent_at, direction, gist):
|
||||
return f"{_ACTIVITY_MARKER} {_fmt_activity_date(sent_at)} — {direction}: {gist}"
|
||||
# Public mailbox providers: a teammate's enrolled gmail must NOT make every gmail sender read
|
||||
# as "ours" (that would flip inbound LP gmail to outbound). Only corporate domains imply us.
|
||||
_PUBLIC_EMAIL_DOMAINS = {
|
||||
"gmail.com", "googlemail.com", "outlook.com", "hotmail.com", "live.com", "msn.com",
|
||||
"yahoo.com", "ymail.com", "icloud.com", "me.com", "mac.com", "aol.com",
|
||||
"proton.me", "protonmail.com", "pm.me",
|
||||
}
|
||||
|
||||
|
||||
def _our_email_domains(own_addresses):
|
||||
"""Corporate domains we send from, derived from the enrolled mailboxes (public providers
|
||||
excluded). Lets an @ten31.xyz sender read as outbound even when that mailbox isn't enrolled —
|
||||
the misclassification behind a teammate's email showing as 'Received'."""
|
||||
out = set()
|
||||
for a in own_addresses:
|
||||
if "@" in a:
|
||||
dom = a.split("@", 1)[1]
|
||||
if dom and dom not in _PUBLIC_EMAIL_DOMAINS:
|
||||
out.add(dom)
|
||||
return out
|
||||
|
||||
|
||||
def _sender_display(from_name, from_email):
|
||||
"""A human name for the sender: their display name, else the email's local part, else a stub."""
|
||||
if from_name and from_name.strip():
|
||||
return from_name.strip()
|
||||
if from_email and "@" in from_email:
|
||||
return from_email.split("@", 1)[0]
|
||||
return (from_email or "").strip() or "Someone"
|
||||
|
||||
|
||||
def _activity_note_text(sent_at, outbound, gist, sender, investor_name):
|
||||
"""Name WHO emailed WHOM rather than a bare Sent/Received (which left 'who received?'
|
||||
ambiguous): outbound -> "{teammate} emailed {investor}", inbound -> "{sender} emailed the team"."""
|
||||
if outbound:
|
||||
rel = f"{sender} emailed {investor_name}" if investor_name else f"{sender} sent an email"
|
||||
else:
|
||||
rel = f"{sender} emailed the team"
|
||||
return f"{_ACTIVITY_MARKER} {_fmt_activity_date(sent_at)} — {rel}: {gist}"
|
||||
|
||||
|
||||
def propose_email_activity_notes(limit=50):
|
||||
@@ -5752,7 +5789,7 @@ def propose_email_activity_notes(limit=50):
|
||||
conn.commit()
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT id, subject, body_text, snippet, from_email, sent_at FROM emails "
|
||||
"SELECT id, subject, body_text, snippet, from_name, from_email, sent_at FROM emails "
|
||||
"WHERE is_matched=1 AND sent_at >= ? "
|
||||
"AND id NOT IN (SELECT email_id FROM email_activity_proposals) "
|
||||
"ORDER BY sent_at ASC LIMIT ?", (since, limit)).fetchall()
|
||||
@@ -5765,14 +5802,19 @@ def propose_email_activity_notes(limit=50):
|
||||
own = {(r[0] or "").lower() for r in conn.execute("SELECT email_address FROM email_accounts")}
|
||||
except Exception:
|
||||
pass
|
||||
our_domains = _our_email_domains(own)
|
||||
done = 0
|
||||
for r in rows:
|
||||
inv_id, inv_name = _activity_investor(conn, r["id"])
|
||||
direction = "Sent" if (r["from_email"] or "").lower() in own else "Received"
|
||||
from_email = (r["from_email"] or "").lower()
|
||||
# Outbound = sent by us: an enrolled mailbox, OR any address on our corporate domain
|
||||
# (covers teammates whose mailbox isn't enrolled — the screenshot's "Received" bug).
|
||||
outbound = from_email in own or (from_email.rsplit("@", 1)[-1] in our_domains if "@" in from_email else False)
|
||||
direction = "Sent" if outbound else "Received"
|
||||
gist = _summarize_email_gist(r["subject"], r["body_text"] or r["snippet"] or "")
|
||||
if not gist:
|
||||
continue # leave unproposed; a later pass retries once the model answers
|
||||
note = _activity_note_text(r["sent_at"], direction, gist)
|
||||
note = _activity_note_text(r["sent_at"], outbound, gist, _sender_display(r["from_name"], r["from_email"]), inv_name)
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO email_activity_proposals "
|
||||
"(id,email_id,investor_id,investor_name,direction,summary,proposed_note,"
|
||||
|
||||
Reference in New Issue
Block a user