feat: add setting to toggle alphabetical grouping in contacts list

This commit is contained in:
Linus Rath
2026-04-21 23:22:18 +02:00
parent 7d6a3c8c76
commit 76f6149841
4 changed files with 67 additions and 37 deletions
+44 -36
View File
@@ -137,6 +137,7 @@ export function ContactList({
const t = useTranslations("contacts"); const t = useTranslations("contacts");
const locale = useLocale(); const locale = useLocale();
const density = useSettingsStore((state) => state.density); const density = useSettingsStore((state) => state.density);
const groupByLetter = useSettingsStore((state) => state.groupContactsByLetter);
const { contextMenu, openContextMenu, closeContextMenu, menuRef } = useContextMenu<ContactCard>(); const { contextMenu, openContextMenu, closeContextMenu, menuRef } = useContextMenu<ContactCard>();
const [filtersOpen, setFiltersOpen] = useState(false); const [filtersOpen, setFiltersOpen] = useState(false);
const [filters, setFilters] = useState<ListFilters>(EMPTY_FILTERS); const [filters, setFilters] = useState<ListFilters>(EMPTY_FILTERS);
@@ -528,43 +529,50 @@ export function ContactList({
)} )}
</div> </div>
) : ( ) : (
<div> (() => {
{groupedSections.map(({ letter, items }) => ( const renderItem = (contact: ContactCard) => (
<section key={letter}> <ContactListItem
<div className="sticky top-0 z-10 px-4 py-0.5 bg-background/90 backdrop-blur-sm text-[11px] font-medium text-muted-foreground/70 uppercase tracking-wide"> key={contact.id}
{letter} contact={contact}
</div> isSelected={contact.id === selectedContactId}
{items.map((contact) => ( isChecked={selectedContactIds.has(contact.id)}
<ContactListItem hasSelection={hasSelection}
key={contact.id} density={density}
contact={contact} selectedContactIds={selectedContactIds}
isSelected={contact.id === selectedContactId} onClick={(e) => {
isChecked={selectedContactIds.has(contact.id)} if (e.ctrlKey || e.metaKey) {
hasSelection={hasSelection} e.preventDefault();
density={density} onToggleSelection(contact.id);
selectedContactIds={selectedContactIds} } else if (e.shiftKey) {
onClick={(e) => { e.preventDefault();
if (e.ctrlKey || e.metaKey) { onSelectRangeContacts(contact.id, sortedIds);
e.preventDefault(); } else {
onToggleSelection(contact.id); if (hasSelection) onClearSelection();
} else if (e.shiftKey) { onSelectContact(contact.id);
e.preventDefault(); }
onSelectRangeContacts(contact.id, sortedIds); }}
} else { onCheckboxClick={(e) => {
if (hasSelection) onClearSelection(); e.stopPropagation();
onSelectContact(contact.id); onToggleSelection(contact.id);
} }}
}} onContextMenu={(e, c) => openContextMenu(e, c)}
onCheckboxClick={(e) => { />
e.stopPropagation(); );
onToggleSelection(contact.id); return groupByLetter ? (
}} <div>
onContextMenu={(e, c) => openContextMenu(e, c)} {groupedSections.map(({ letter, items }) => (
/> <section key={letter}>
<div className="sticky top-0 z-10 px-4 py-0.5 bg-background/90 backdrop-blur-sm text-[11px] font-medium text-muted-foreground/70 uppercase tracking-wide">
{letter}
</div>
{items.map(renderItem)}
</section>
))} ))}
</section> </div>
))} ) : (
</div> <div>{sorted.map(renderItem)}</div>
);
})()
)} )}
</div> </div>
+14 -1
View File
@@ -4,11 +4,12 @@ import { useState, useCallback } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Upload, Download } from "lucide-react"; import { Upload, Download } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { SettingsSection, SettingItem } from "./settings-section"; import { SettingsSection, SettingItem, ToggleSwitch } from "./settings-section";
import { ContactImportDialog } from "@/components/contacts/contact-import-dialog"; import { ContactImportDialog } from "@/components/contacts/contact-import-dialog";
import { exportContacts } from "@/components/contacts/contact-export"; import { exportContacts } from "@/components/contacts/contact-export";
import { useContactStore } from "@/stores/contact-store"; import { useContactStore } from "@/stores/contact-store";
import { useAuthStore } from "@/stores/auth-store"; import { useAuthStore } from "@/stores/auth-store";
import { useSettingsStore } from "@/stores/settings-store";
import { toast } from "@/stores/toast-store"; import { toast } from "@/stores/toast-store";
export function ContactsSettings() { export function ContactsSettings() {
@@ -20,6 +21,8 @@ export function ContactsSettings() {
supportsSync, supportsSync,
importContacts, importContacts,
} = useContactStore(); } = useContactStore();
const groupContactsByLetter = useSettingsStore((s) => s.groupContactsByLetter);
const updateSetting = useSettingsStore((s) => s.updateSetting);
const [showImport, setShowImport] = useState(false); const [showImport, setShowImport] = useState(false);
const individuals = contacts.filter(c => c.kind !== "group"); const individuals = contacts.filter(c => c.kind !== "group");
@@ -55,6 +58,16 @@ export function ContactsSettings() {
title={tSettings("title")} title={tSettings("title")}
description={tSettings("description")} description={tSettings("description")}
> >
<SettingItem
label={tSettings("group_by_letter_label")}
description={tSettings("group_by_letter_description")}
>
<ToggleSwitch
checked={groupContactsByLetter}
onChange={(checked) => updateSetting("groupContactsByLetter", checked)}
/>
</SettingItem>
<SettingItem <SettingItem
label={tSettings("import_label")} label={tSettings("import_label")}
description={tSettings("import_description")} description={tSettings("import_description")}
+2
View File
@@ -1322,6 +1322,8 @@
"contacts": { "contacts": {
"title": "Contacts", "title": "Contacts",
"description": "Import and export your contacts", "description": "Import and export your contacts",
"group_by_letter_label": "Group by first letter",
"group_by_letter_description": "Show alphabetical section headers in the contact list",
"import_label": "Import Contacts", "import_label": "Import Contacts",
"import_description": "Import contacts from a vCard (.vcf) file", "import_description": "Import contacts from a vCard (.vcf) file",
"export_label": "Export Contacts", "export_label": "Export Contacts",
+7
View File
@@ -160,6 +160,9 @@ interface SettingsState {
showBirthdayCalendar: boolean; showBirthdayCalendar: boolean;
birthdayCalendarColor: string; birthdayCalendarColor: string;
// Contacts Display
groupContactsByLetter: boolean;
// Email Notifications // Email Notifications
emailNotificationsEnabled: boolean; emailNotificationsEnabled: boolean;
emailNotificationSound: boolean; emailNotificationSound: boolean;
@@ -304,6 +307,9 @@ const DEFAULT_SETTINGS = {
showBirthdayCalendar: false, showBirthdayCalendar: false,
birthdayCalendarColor: '#eab308', birthdayCalendarColor: '#eab308',
// Contacts Display
groupContactsByLetter: true,
// Email Notifications // Email Notifications
emailNotificationsEnabled: true, emailNotificationsEnabled: true,
emailNotificationSound: true, emailNotificationSound: true,
@@ -459,6 +465,7 @@ export const useSettingsStore = create<SettingsState>()(
showTasksOnCalendar: state.showTasksOnCalendar, showTasksOnCalendar: state.showTasksOnCalendar,
showBirthdayCalendar: state.showBirthdayCalendar, showBirthdayCalendar: state.showBirthdayCalendar,
birthdayCalendarColor: state.birthdayCalendarColor, birthdayCalendarColor: state.birthdayCalendarColor,
groupContactsByLetter: state.groupContactsByLetter,
expandedFilterView: state.expandedFilterView, expandedFilterView: state.expandedFilterView,
showTimeInMonthView: state.showTimeInMonthView, showTimeInMonthView: state.showTimeInMonthView,
showWeekNumbers: state.showWeekNumbers, showWeekNumbers: state.showWeekNumbers,