Skip to content

feat: add local contacts and vCard support (#81) - #89

Merged
QingJ01 merged 17 commits into
masterfrom
codex/issue-81-contacts
Aug 10, 2026
Merged

feat: add local contacts and vCard support (#81)#89
QingJ01 merged 17 commits into
masterfrom
codex/issue-81-contacts

Conversation

@QingJ01

@QingJ01 QingJ01 commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • add a local, searchable contact book with multi-address contacts, favorites, notes, participant actions, and transactional CRUD
  • prioritize saved contacts in recipient autocomplete while preserving recent-recipient fallback and suppression
  • add vCard import/export with validation, duplicate merging, partial error reporting, and standards-compatible field handling
  • include contacts in backup schema v2 while preserving v1 compatibility and enforcing restore-safe size/schema validation
  • document the new Contacts workflow in English and Chinese release documentation

Independent review

An independent sub-agent review found five issues around backup safety, pagination, accessibility, and quoted vCard parameters. All five were fixed with regression coverage. The final re-review reported no Critical, Important, or Minor findings.

Verification

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • pnpm test -- --reporter=dot — 94 files, 360 tests passed
  • pnpm build:frontend
  • cargo test --workspace --exclude pebble-oauth
  • git diff --check

The existing pebble-oauth test token_exchange_without_secret_uses_public_client_request still fails on the base branch. This PR does not modify crates/pebble-oauth.

Resolves #81

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 7, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
pebble 52350da Commit Preview URL

Branch Preview URL
Aug 10 2026, 05:13 AM

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 372b30e184

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/pebble-store/src/cloud_sync.rs Outdated
Comment on lines +340 to +343
loop {
let page = self.list_contacts(None, false, 200, contacts.len() as i64)?;
let page_len = page.len();
contacts.extend(page);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Read paginated contacts from one backup snapshot

When a profile has more than 200 contacts and a contact is added, deleted, or renamed while an automatic or manual backup is paging, each list_contacts call runs as an independent read. An edit that moves rows before the current offset can therefore skip a contact or duplicate one in the backup; the latter can make the entire restore fail on the duplicate contact ID. Load all contact IDs through one read transaction/snapshot rather than advancing an offset across separate reads.

Useful? React with 👍 / 👎.

Comment on lines +224 to +227
let card_count = unfolded
.iter()
.filter(|line| line.eq_ignore_ascii_case("BEGIN:VCARD"))
.count();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject imports that contain no vCard records

For an empty, unrelated, or malformed file with no BEGIN:VCARD, card_count is zero and this function returns an empty successful result. The UI consequently reports “vCard import complete” with zero invalid records, so users cannot distinguish a valid empty operation from selecting the wrong or corrupted file. Return a validation error when no card marker is found.

Useful? React with 👍 / 👎.

Comment thread crates/pebble-store/src/contacts.rs Outdated
Comment on lines +414 to +418
WHERE (?1 = '' OR c.display_name LIKE ?2 ESCAPE '\\' COLLATE NOCASE
OR EXISTS (
SELECT 1 FROM contact_emails ce
WHERE ce.contact_id = c.id
AND ce.address LIKE ?2 ESCAPE '\\' COLLATE NOCASE

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make contact search case-insensitive for non-ASCII names

SQLite's built-in NOCASE collation only folds ASCII characters, so a saved contact named Élodie is not returned for the natural lowercase query élodie. The same SQL pattern is used for saved compose suggestions, causing international contacts to disappear or be demoted to recent-history results. Normalize searchable values with Unicode-aware case folding, or perform the comparison through a Unicode-aware collation.

Useful? React with 👍 / 👎.

Comment on lines +116 to +117
if (notes.trim().length > 2000) {
return t("contacts.notesTooLong", "Notes must be 2000 characters or fewer");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count note characters consistently across the UI and store

JavaScript length and the textarea's maxLength count UTF-16 code units, while the backend enforces the limit with Rust chars().count(). Consequently a backend-valid note containing 1,001–2,000 emoji or other supplementary characters cannot be entered or resaved in this dialog; this also makes an imported contact with such a note uneditable until text is removed. Apply the same character-count definition as the backend and avoid the conflicting native maxLength limit.

Useful? React with 👍 / 👎.

Comment thread crates/pebble-store/src/vcard.rs Outdated
Comment on lines +168 to +169
if version.as_deref().is_some_and(|value| value != "3.0") {
return Err("Only vCard 3.0 is supported".to_string());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require VERSION before parsing a vCard as 3.0

A card with no VERSION property bypasses this condition and is imported as though it were supported vCard 3.0. Because VERSION is required and determines the syntax and encoding rules, a malformed or version-stripped card can be silently interpreted incorrectly instead of appearing in the partial-error report. Reject None as well as versions other than 3.0.

Useful? React with 👍 / 👎.

Comment on lines +50 to +52
|| domain.starts_with('.')
|| domain.ends_with('.')
|| !domain.contains('.')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject empty labels inside email domains

Addresses such as user@example..com pass both the frontend regex and this backend validation because the domain merely has to contain a dot and not start or end with one. Such a contact can therefore be saved or imported and offered by recipient autocomplete even though the address has an invalid empty domain label, causing failure only when the user tries to send mail. Validate each domain label rather than only the outer dots.

Useful? React with 👍 / 👎.

Comment on lines +67 to +69
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") onClose();
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep Escape disabled while a contact save is pending

When a save is slow, the close button and backdrop correctly prevent dismissal via isSaving, but this document-level Escape handler still closes the dialog. If the pending save then fails, the dialog that would display the error has already unmounted and the user's entered contact data is lost without feedback. Ignore Escape while saving, consistently with the other dismissal paths.

Useful? React with 👍 / 👎.

@QingJ01
QingJ01 merged commit 00e3bde into master Aug 10, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

【功能建议】增加联系人功能

1 participant