Productivity features, UI translations, local email translation
- 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>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user