Skip to content

feat(contacts): refactor contacts#3995

Open
devin-ai-integration[bot] wants to merge 6 commits intomainfrom
devin/1771176307-contacts-refactor
Open

feat(contacts): refactor contacts#3995
devin-ai-integration[bot] wants to merge 6 commits intomainfrom
devin/1771176307-contacts-refactor

Conversation

@devin-ai-integration
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot commented Feb 15, 2026

Summary

Refactors the contacts system by replacing the old { selectedOrganization, selectedPerson } state shape with a tagged union ContactsSelection ({ type: "person" | "organization", id }), and adds pinning/ordering support for both people and organizations.

Key changes:

  • New ContactsSelection enum in Rust (state.rs) and auto-generated TypeScript bindings
  • New contacts-list.tsx (~600 LOC) with combined pinned list, drag-to-reorder (via motion/react), search hotkey, and sort options
  • Added created_at, pinned, pin_order fields to organization schema (zod + tinybase + queries)
  • Added created_at, pin_order to human transform/frontmatter logic
  • Removed contacts permission check from calendar sidebar
  • Updated all consumers of ContactsState (advanced-search, calendar-view, tab schema, tests)
  • Removed now-dead organizations.tsx and people.tsx (replaced by contacts-list.tsx)

Updates since initial revision

Addressed bot review comments:

  • Dead code removed: Deleted organizations.tsx and people.tsx — no longer imported after the switch to contacts-list.tsx.
  • pin_order clearing fixed: Changed pin_order: undefined to pin_order: 0 when unpinning in contacts-list.tsx and details.tsx. TinyBase's setPartialRow ignores undefined values, so the old pin_order was silently persisting on unpinned items.
  • details.tsx maxOrder bug fixed: handleTogglePin in the detail panel now computes maxOrder across both humans and organizations tables (previously only checked humans), matching the behavior in contacts-list.tsx.
  • Drag-reorder disabled during search: When searchValue is non-empty, pinned items render in a plain <div> instead of a <Reorder.Group>, preventing pin_order collisions between visible and hidden pinned items. Reorder remains fully functional when no search filter is active.
  • Dead NewOrgForm code removed: Deleted the showNewOrg state, the conditional <NewOrgForm> render block, and the entire NewOrgForm component (~70 lines). The handleAdd callback only opens the person form, so the org form was unreachable.

Merge conflict resolution

Merged latest main to resolve conflicts:

  • calendar-view.tsx: Main refactored this file significantly (removed CalendarSidebarContent entirely). The PR's change (removing contacts permission check) is now moot — took main's version.
  • people.tsx: PR deleted this file (replaced by contacts-list.tsx), main added Facehash avatars to it. Kept deleted and ported the Facehash avatar change to contacts-list.tsx.
  • contacts-list.tsx: Now uses <Facehash> component for person avatars instead of getInitials, matching main's avatar style.

Review & Testing Checklist for Human

  • Tab state migration: The ContactsState shape changed from { selectedOrganization, selectedPerson } to { selected: ContactsSelection | null }. There is no migration logic. Old persisted tabs will have selected: undefined at runtime — the auto-select useEffect masks this, but the old selection is silently lost. Verify no runtime errors on app load with pre-existing contacts tabs.
  • Facehash avatar in contacts-list: The <Facehash> integration was ported from main's people.tsx change but has not been visually tested. Verify person avatars render correctly in the contacts list (props: size={32}, interactive={false}, showInitial={false}).
  • No component-level tests: All new UI code (~600 LOC in contacts-list.tsx) has zero component tests. Verification is limited to typecheck + transform unit tests. Manual testing is essential.
  • created_at sort for organizations: Existing organizations won't have created_at populated. Verify "Newest" and "Oldest" sort options produce sensible order for pre-existing data.
  • Search + reorder UX: Drag-reorder is now disabled during search. Verify that pinned items still display correctly when filtered by search, and that the transition (items becoming non-draggable) is not confusing. Verify reorder resumes working after clearing search.

Suggested test plan: Open the contacts tab, create a few people and organizations, pin some of each, reorder the pinned list via drag, switch sort options, use Cmd+F search (verify reorder is disabled and items remain visible), clear search (verify reorder works again), close and reopen the app, and verify selection state + pin order are preserved. Also try pinning from the detail panel and confirm the item appears at the end of the combined pinned list. Verify person avatars display correctly with Facehash.

Notes

  • This is a clean re-creation of #3818 on top of current main to eliminate merge conflict noise.
  • Transform tests now assert pin_order round-trip for both human and organization transforms.
  • Desktop typecheck passes. CI verification in progress.

Link to Devin run: https://app.devin.ai/sessions/20cd5306dbec46ed836175a3e6487227
Requested by: @ComputelessComputer

…n/ordering support

Co-Authored-By: john@hyprnote.com <john@hyprnote.com>
@devin-ai-integration
Copy link
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@netlify
Copy link

netlify bot commented Feb 15, 2026

Deploy Preview for hyprnote ready!

Name Link
🔨 Latest commit f45c529
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote/deploys/6994badea512270008801c4a
😎 Deploy Preview https://deploy-preview-3995--hyprnote.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Feb 15, 2026

Deploy Preview for hyprnote-storybook canceled.

Name Link
🔨 Latest commit f45c529
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote-storybook/deploys/6994bade03580e00080fe43c

Co-Authored-By: john@hyprnote.com <john@hyprnote.com>
Copy link
Contributor Author

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 5 potential issues.

⚠️ 1 issue in files not directly in the diff

⚠️ details.tsx handleTogglePin computes maxOrder from humans table only, not both tables (apps/desktop/src/components/main/body/contacts/details.tsx:123-131)

In the new combined contacts list (contacts-list.tsx), pinning computes maxOrder across both humans and organizations tables so newly-pinned items sort to the end of the combined list. However, the person detail panel in details.tsx:113-132 still computes maxOrder from the humans table alone. A user pinning a person via the detail view gets a pin_order that ignores organizations' pin_order values, causing the item to appear in the wrong position.

Detailed Explanation

The new contacts-list.tsx correctly checks both tables when pinning (e.g., contacts-list.tsx:347-359 for PersonItem):

const maxHumanOrder = Object.values(allHumans).reduce(...);
const maxOrgOrder = Object.values(allOrgs).reduce(...);
pin_order: Math.max(maxHumanOrder, maxOrgOrder) + 1;

But details.tsx:123-131 only checks humans:

const allHumans = store.getTable("humans");
const maxOrder = Object.values(allHumans).reduce((max, h) => {
  const order = (h.pin_order as number | undefined) ?? 0;
  return Math.max(max, order);
}, 0);
store.setPartialRow("humans", selectedHumanId, {
  pinned: true,
  pin_order: maxOrder + 1,
});

Example: Organization "Acme" is pinned at pin_order: 5, max human pin_order is 2. Pinning person "John" from the detail view gives pin_order: 3. In the combined pinned list, John appears before Acme (3 < 5) instead of at the end.

Impact: Newly pinned persons from the detail view may not appear at the end of the combined pinned list, violating the expected "pin adds to end" behavior described in the PR.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines 132 to 137
...base,
type: "contacts",
state: tab.state ?? {
selectedOrganization: null,
selectedPerson: null,
selected: null,
},
};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

🚩 No tab state migration for persisted ContactsState shape change

The ContactsState shape changed from { selectedOrganization: string | null, selectedPerson: string | null } to { selected: ContactsSelection | null }. Pinned tabs are serialized/deserialized via pinned-persistence.ts:15-35, which uses JSON.stringify/JSON.parse with no schema validation or migration. If a user has a pinned contacts tab saved with the old shape ({ selectedOrganization, selectedPerson }), on next app load restorePinnedTabsToStore will restore that tab with the old state shape. The getDefaultState in schema.ts:130-137 only applies when tab.state is null/undefined — if the old state object is present, it's used as-is. This means tab.state.selected would be undefined at runtime, and tab.state.selectedOrganization/selectedPerson would be present but unused. The auto-select useEffect in contacts/index.tsx:125-129 would then trigger (since selected is falsy), masking the issue. So this likely won't crash but the old persisted selection is silently lost.

(Refers to lines 130-137)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 8f3d029details.tsx handleTogglePin now computes maxOrder across both humans and organizations tables, matching the behavior in contacts-list.tsx.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged — the auto-select useEffect masks this so it won't crash, just silently resets the old selection. Noted in the PR description checklist.

…ng, fix details maxOrder

Co-Authored-By: john@hyprnote.com <john@hyprnote.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: john@hyprnote.com <john@hyprnote.com>
Copy link
Contributor Author

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 8 additional findings in Devin Review.

Open in Devin Review

Comment on lines 125 to +129
useEffect(() => {
if (!selectedOrganization && !selectedPerson && humanIds.length > 0) {
setSelectedPerson(humanIds[0]);
if (!selected && allHumanIds.length > 0) {
setSelected({ type: "person", id: allHumanIds[0] });
}
}, [humanIds, selectedPerson, selectedOrganization, setSelectedPerson]);

const isViewingOrgDetails = selectedOrganization && !selectedPerson;
}, [allHumanIds, selected, setSelected]);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

🚩 Auto-select useEffect only selects persons, never organizations

The auto-select useEffect at line 125-129 only queries visibleHumans and auto-selects a person when selected is null. If the contact list contains only organizations and no people, no item will be auto-selected and the detail panel will show "Select a person to view details". This is a design choice (the old code also only auto-selected people), but may be surprising now that organizations are first-class items in the combined list.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

devin-ai-integration bot and others added 2 commits February 16, 2026 06:40
Co-Authored-By: john@hyprnote.com <john@hyprnote.com>
…list

Co-Authored-By: john@hyprnote.com <john@hyprnote.com>
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