Skip to content

Google Contacts Initial Release #17638

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open

Google Contacts Initial Release #17638

wants to merge 11 commits into from

Conversation

d0rN834
Copy link

@d0rN834 d0rN834 commented Mar 7, 2025

Description

Manage your Google Contacts directly from Raycast with complete integration, including AI capabilities.

Core Features

  • πŸ” Search contacts by name, email, or phone number
  • πŸ‘οΈ View detailed contact information
  • ⭐ Mark contacts as favorites for quick access
  • βž• Create new contacts with comprehensive details
  • ✏️ Edit existing contacts (all fields supported)
  • πŸ—‘οΈ Delete contacts
  • πŸ”„ Sync with Google Contacts API

Screencast

See Screenshots. Won't share personal data.

Checklist

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

PR Summary

This PR adds a comprehensive Google Contacts extension for Raycast with contact management and AI capabilities. Here are the key points to address:

  • The CHANGELOG.md contains incorrect entries for Google Tasks - it should be replaced with a single "Initial Version" entry ending with {PR_MERGE_DATE}

  • The README.md has incorrect store/GitHub links pointing to Microsoft Teams instead of Google Contacts and contains a double period in the description

  • Since there are view commands in package.json, a metadata folder with screenshots should be added per Raycast guidelines

  • The tools section in package.json needs to include ai with evals as required by Raycast's AI extension guidelines

  • The launchCommand in create-contact.tsx should be wrapped in a try-catch block per Raycast best practices

The extension implementation itself appears solid with comprehensive features and good error handling, but these documentation and configuration issues should be addressed before merging.

πŸ’‘ (2/5) Greptile learns from your feedback when you react with πŸ‘/πŸ‘Ž!

18 file(s) reviewed, 43 comment(s)
Edit PR Review Bot Settings | Greptile

Comment on lines 29 to 136
"query": "John Smith"
},
{
"query": "acme corporation",
"limit": 3
}
],
"arguments": [
{
"name": "query",
"title": "Search Query",
"required": true,
"description": "The search term to look for in contacts (name, email, company, etc.)"
},
{
"name": "limit",
"title": "Result Limit",
"required": false,
"description": "Maximum number of contacts to return (default: 5)"
}
]
},
{
"path": "./src/tools/create-contact",
"name": "create-contact",
"title": "Create Google Contact",
"description": "Create a new contact in your Google Contacts.",
"examples": [
{
"firstName": "John",
"lastName": "Smith",
"email": "[email protected]",
"phone": "+1 555-123-4567"
},
{
"firstName": "Jane",
"lastName": "Doe",
"company": "Acme Corporation",
"jobTitle": "Marketing Manager",
"email": "[email protected]"
}
],
"arguments": [
{
"name": "firstName",
"title": "First Name",
"required": true,
"description": "The contact's first name"
},
{
"name": "lastName",
"title": "Last Name",
"required": false,
"description": "The contact's last name"
},
{
"name": "email",
"title": "Email",
"required": false,
"description": "The contact's email address"
},
{
"name": "phone",
"title": "Phone",
"required": false,
"description": "The contact's phone number"
},
{
"name": "company",
"title": "Company",
"required": false,
"description": "The contact's company or organization"
},
{
"name": "jobTitle",
"title": "Job Title",
"required": false,
"description": "The contact's job title or position"
},
{
"name": "address",
"title": "Address",
"required": false,
"description": "The contact's physical address"
},
{
"name": "birthday",
"title": "Birthday",
"required": false,
"description": "The contact's birthday in DD.MM.YYYY format"
},
{
"name": "notes",
"title": "Notes",
"required": false,
"description": "Additional notes about the contact"
}
]
}
],
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: AI tools are defined but missing required 'ai' field with 'evals'. Please add evals to test the AI functionality. See: https://developers.raycast.com/ai/write-evals-for-your-ai-extension

Comment on lines 17 to 27
"name": "view-contacts",
"title": "List Contacts",
"description": "View and manage your Google Contacts",
"mode": "view"
},
{
"name": "create-contact",
"title": "Create Contact",
"description": "Create a new Google Contact",
"mode": "view"
}
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Consider adding 'subtitle' field to commands to show 'Google Contacts' as service name for better context

@@ -0,0 +1,14 @@
# Google Tasks Changelog
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: This should be 'Google Contacts Changelog' instead of 'Google Tasks Changelog' since this is for the Google Contacts extension

Suggested change
# Google Tasks Changelog
# Google Contacts Changelog

Comment on lines 3 to 8
## [Sort tasks by due date] - 2023-07-23

## [Update] - 2023-05-12

- Increase the maximum tasks from 20 to 100.
- Modify the filtering logic. This will now display up to 100 open or completed tasks.
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: These entries appear to be for Google Tasks and should be removed since this is a new Google Contacts extension

Comment on lines 12 to 14
## [Initial Version] - 2022-09-16

Initial version code
Copy link
Contributor

Choose a reason for hiding this comment

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

syntax: For a new extension, there should be a single entry like:

Suggested change
## [Initial Version] - 2022-09-16
Initial version code
## [Initial Version] {PR_MERGE_DATE}
- Initial version of Google Contacts extension

Comment on lines 181 to 221
// Format a birthday from a ContactBirthday object
export function formatBirthday(birthday: ContactBirthday): string {
// If there's a text representation, use that
if (birthday.text) {
return birthday.text;
}

// If there's date information, format it
if (birthday.date) {
const { year, month, day } = birthday.date;

// Format: dd.MM.yyyy or dd.MM depending on if year is available
if (month && day) {
const monthStr = month.toString().padStart(2, '0');
const dayStr = day.toString().padStart(2, '0');

if (year) {
return `${dayStr}.${monthStr}.${year}`;
} else {
return `${dayStr}.${monthStr}`;
}
}
}

return "Unknown";
}

// Get birthday info from a contact
export function getBirthdayInfo(contact: Contact): string | undefined {
if (!contact.birthdays || contact.birthdays.length === 0) {
return undefined;
}

// Try to find the primary birthday if there are multiple
const primaryBirthday = contact.birthdays.find(birthday => birthday.metadata?.primary);

// If primary birthday exists, use it, otherwise use the first birthday
const birthdayToUse = primaryBirthday || contact.birthdays[0];

return formatBirthday(birthdayToUse);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

syntax: formatBirthday and getBirthdayInfo functions are incorrectly indented - they should be at the root level

Suggested change
// Format a birthday from a ContactBirthday object
export function formatBirthday(birthday: ContactBirthday): string {
// If there's a text representation, use that
if (birthday.text) {
return birthday.text;
}
// If there's date information, format it
if (birthday.date) {
const { year, month, day } = birthday.date;
// Format: dd.MM.yyyy or dd.MM depending on if year is available
if (month && day) {
const monthStr = month.toString().padStart(2, '0');
const dayStr = day.toString().padStart(2, '0');
if (year) {
return `${dayStr}.${monthStr}.${year}`;
} else {
return `${dayStr}.${monthStr}`;
}
}
}
return "Unknown";
}
// Get birthday info from a contact
export function getBirthdayInfo(contact: Contact): string | undefined {
if (!contact.birthdays || contact.birthdays.length === 0) {
return undefined;
}
// Try to find the primary birthday if there are multiple
const primaryBirthday = contact.birthdays.find(birthday => birthday.metadata?.primary);
// If primary birthday exists, use it, otherwise use the first birthday
const birthdayToUse = primaryBirthday || contact.birthdays[0];
return formatBirthday(birthdayToUse);
}
// Format a birthday from a ContactBirthday object
export function formatBirthday(birthday: ContactBirthday): string {
// If there's a text representation, use that
if (birthday.text) {
return birthday.text;
}
// If there's date information, format it
if (birthday.date) {
const { year, month, day } = birthday.date;
// Format: dd.MM.yyyy or dd.MM depending on if year is available
if (month && day) {
const monthStr = month.toString().padStart(2, '0');
const dayStr = day.toString().padStart(2, '0');
if (year) {
return `${dayStr}.${monthStr}.${year}`;
} else {
return `${dayStr}.${monthStr}`;
}
}
}
return "Unknown";
}
// Get birthday info from a contact
export function getBirthdayInfo(contact: Contact): string | undefined {
if (!contact.birthdays || contact.birthdays.length === 0) {
return undefined;
}
// Try to find the primary birthday if there are multiple
const primaryBirthday = contact.birthdays.find(birthday => birthday.metadata?.primary);
// If primary birthday exists, use it, otherwise use the first birthday
const birthdayToUse = primaryBirthday || contact.birthdays[0];
return formatBirthday(birthdayToUse);
}

// Cache keys
const CONTACTS_CACHE_KEY = "google_contacts_cached_data";
const CACHE_TIMESTAMP_KEY = "google_contacts_cache_timestamp";
const CACHE_EXPIRY_TIME = 60 + 24 * 60 * 1000; // 1 Day in milliseconds
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Cache expiry time calculation is incorrect - should be 24 * 60 * 60 * 1000 for 1 day in milliseconds

Suggested change
const CACHE_EXPIRY_TIME = 60 + 24 * 60 * 1000; // 1 Day in milliseconds
const CACHE_EXPIRY_TIME = 24 * 60 * 60 * 1000; // 1 Day in milliseconds

Comment on lines 80 to 81
await google.authorize();
const allContacts = await fetchContacts(1000);
Copy link
Contributor

Choose a reason for hiding this comment

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

style: google.authorize() should be wrapped in try/catch to handle auth failures gracefully

// Only show error toast if we don't have cached contacts to show
if (contacts.length === 0) {
setIsLoading(false);
showToast({ style: Toast.Style.Failure, title: String(error) });
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Could be simplified using showFailureToast from @raycast/utils

Suggested change
showToast({ style: Toast.Style.Failure, title: String(error) });
showFailureToast(String(error));

Comment on lines 111 to 113
if (isLoading && contacts.length === 0) {
return <Detail isLoading={true} markdown="Loading contacts..." />;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Consider using List with isLoading prop instead of Detail to avoid empty state flicker

Suggested change
if (isLoading && contacts.length === 0) {
return <Detail isLoading={true} markdown="Loading contacts..." />;
}
if (isLoading && contacts.length === 0) {
return <ContactsView initialContacts={[]} filter={Filter.All} isLoading={true} />;
}

@raycastbot raycastbot added the new extension Label for PRs with new extensions label Mar 13, 2025
@raycastbot
Copy link
Collaborator

Congratulations on your new Raycast extension! πŸš€

Due to our current reduced availability, the initial review may take up to 10-15 business days

Once the PR is approved and merged, the extension will be available on our Store.

@pernielsentikaer pernielsentikaer added AI Extension and removed new extension Label for PRs with new extensions labels Mar 13, 2025
@raycastbot raycastbot added the new extension Label for PRs with new extensions label Mar 14, 2025
@d0rN834
Copy link
Author

d0rN834 commented Mar 18, 2025

I'm sorry, but I cant resolve the issues. See terminal output attached:
CleanShot 2025-03-18 at 17 32 28

@andreaselia
Copy link
Contributor

Hey @d0rN834 πŸ‘‹

You can run npx ray lint --fix to hopefully automatically resolve your lint issues here.

Let me know if you run into any further issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
AI Extension new extension Label for PRs with new extensions
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants