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,44 @@
|
||||
"""Sentence completion for the compose window (app/completion.py)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app import completion
|
||||
from app.db import get_session
|
||||
from app.identity.deps import CurrentUser, require_csrf
|
||||
from app.models import CompletionIndexState
|
||||
from app.queue import enqueue_job
|
||||
|
||||
router = APIRouter(prefix="/completion", tags=["compose"], dependencies=[Depends(require_csrf)])
|
||||
|
||||
DbSession = Annotated[AsyncSession, Depends(get_session)]
|
||||
|
||||
|
||||
class CompletionIn(BaseModel):
|
||||
# The text before the cursor (its last line is all that matters). A POST,
|
||||
# not a query string: what the user is typing stays out of access logs.
|
||||
before: str = Field(max_length=2000)
|
||||
|
||||
|
||||
class CompletionOut(BaseModel):
|
||||
suggestion: str | None
|
||||
|
||||
|
||||
@router.post("", response_model=CompletionOut)
|
||||
async def suggest(body: CompletionIn, user: CurrentUser, db: DbSession) -> CompletionOut:
|
||||
before = body.before[-300:]
|
||||
built = await db.scalar(
|
||||
select(CompletionIndexState.built_at).where(CompletionIndexState.user_id == user.id)
|
||||
)
|
||||
if built is None or built < dt.datetime.now(dt.UTC) - completion.REBUILD_AFTER:
|
||||
# In the background; until it is built, the starter phrases still answer.
|
||||
await enqueue_job("build_index_job", str(user.id), _job_id=f"complete:{user.id}")
|
||||
personal = await completion.personal_phrases(db, user.id, completion.keys_for(before))
|
||||
return CompletionOut(suggestion=completion.complete(personal, before))
|
||||
Reference in New Issue
Block a user