- Snooze (with a Snoozed folder), keyboard shortcuts with a Settings section, mail templates (Compose > More > Templates), a Subscriptions folder with bulk unsubscribe/delete, and tracker blocking with a Privacy toggle (on by default) - Own dialogs instead of browser popups; emoji picker in compose; compose windows survive a visit to Settings - Sentence completion learned from the user's sent mail (no AI) - Interface in English and Polish (one file per language, English as the fallback), picked from the browser on first run, then kept - Email translation on the server via LibreTranslate: Translate button and "Translate to...", messages in several languages translated part by part, names/addresses/emoji/file names/phone numbers kept, an animated line while translating - Antispam "Mark as spam" loads with the message, not when the menu opens Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
90 lines
3.3 KiB
Python
90 lines
3.3 KiB
Python
"""Sentence completion: phrases from the user's own sent mail, plus starters."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from collections import Counter
|
||
|
||
from app.completion import build_phrases, choose, complete, html_to_text, own_text
|
||
|
||
|
||
def personal(*texts: str) -> dict[str, Counter[str]]:
|
||
out: dict[str, Counter[str]] = {}
|
||
for (ctx, cont), n in build_phrases(texts).items():
|
||
out.setdefault(ctx, Counter())[cont] = n
|
||
return out
|
||
|
||
|
||
def test_own_text_drops_quotes_and_signature() -> None:
|
||
for quote in (
|
||
"On Mon, 5 Oct 2026 Anna wrote:",
|
||
"W dniu 5.10.2026 Anna pisze:",
|
||
"Am 05.10.2026 um 10:00 schrieb Anna:",
|
||
"-----Original Message-----",
|
||
"From: Anna <anna@x.example>",
|
||
"--",
|
||
):
|
||
text = f"Sure, I'll send it today.\n\n{quote}\nOld text here."
|
||
assert own_text(text).strip() == "Sure, I'll send it today.", quote
|
||
assert own_text("Yes.\n> quoted line\nAgreed.") == "Yes.\nAgreed."
|
||
|
||
|
||
def test_only_repeated_phrases_are_kept() -> None:
|
||
phrases = build_phrases(
|
||
["I'll send the drawings by Friday.", "I'll send the drawings by Friday."]
|
||
)
|
||
assert phrases[("the", "drawings by Friday.")] == 2
|
||
once = build_phrases(["A private sentence written once."])
|
||
assert once == {}
|
||
|
||
|
||
def test_personal_phrases_complete_in_any_language() -> None:
|
||
mine = personal(
|
||
"I'll send the drawings by Friday.",
|
||
"I'll send the drawings by Friday.",
|
||
"Prześlę rysunki do piątku.",
|
||
"Prześlę rysunki do piątku.",
|
||
)
|
||
assert complete(mine, "I'll send the dr") == "awings by Friday."
|
||
assert complete(mine, "Prześlę rys") == "unki do piątku."
|
||
assert complete(mine, "Prześlę rysunki ") == "do piątku."
|
||
|
||
|
||
def test_starter_phrases_and_spacing() -> None:
|
||
assert complete({}, "I look forward ") == "to hearing from you."
|
||
assert complete({}, "I look forward") == " to hearing from you." # space added
|
||
assert complete({}, "Z pow") == "ażaniem,"
|
||
assert complete({}, "Mit freundlichen ") == "Grüßen"
|
||
|
||
|
||
def test_nothing_after_punctuation_or_on_an_empty_line() -> None:
|
||
assert complete({}, "Thanks,") is None
|
||
assert complete({}, "Done. ") is None
|
||
assert complete({}, "") is None
|
||
assert complete({}, " ") is None
|
||
|
||
|
||
def test_suggestion_stops_where_habits_part() -> None:
|
||
# "see you" is followed by "soon" or "tomorrow" equally: suggest only "you".
|
||
assert choose(Counter({"you": 4, "you soon": 2, "you tomorrow": 2})) == "you"
|
||
assert choose(Counter({"you": 4, "you soon": 4, "you soon then": 3})) == "you soon then"
|
||
|
||
|
||
def test_polish_gmail_quotes_and_own_signature_are_left_out() -> None:
|
||
text = (
|
||
"Dziękuję, prześlę jutro.\n"
|
||
"Imaginary Pixels – visual effects studio\n"
|
||
"pon., 5 paź 2026 o 10:00 Anna Nowak <anna@x.example> napisał(a):\n"
|
||
"> stary tekst"
|
||
)
|
||
sig = html_to_text(
|
||
"<div>Imaginary Pixels – visual effects studio</div><div>+48 602 691 231</div>"
|
||
)
|
||
assert own_text(text, [sig]).strip() == "Dziękuję, prześlę jutro."
|
||
|
||
|
||
def test_numbers_addresses_and_links_are_never_learned() -> None:
|
||
texts = ["Call me at +48 602 691 231 or write to me@x.example today."] * 3
|
||
learned = " ".join(f"{c} {n}" for (c, n) in build_phrases(texts))
|
||
assert "602" not in learned and "@" not in learned
|
||
assert ("call me", "at") in build_phrases(texts)
|