项目文件夹

文件
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

195 行
5.7 KiB
TypeScript

import { useCallback, useSyncExternalStore } from 'react';
import { createAppStoreWithActions } from '../../os/createAppStore';
import ContentResolver from '../../os/ContentResolver';
import { ensureContactsProviderRegistered } from '../../os/providers/ContactsProvider';
import { CONTACTS_CONFIG } from './data';
import type { Contact, ContactId } from './types';
import * as TimeService from '../../os/TimeService';
// ---- Helper functions ----
export type PhoneSettingsValue = string | number | boolean | null;
interface ContactsState {
searchHistory: string[];
settings: Record<string, PhoneSettingsValue>;
}
interface ContactsActions {
addSearchHistory: (query: string) => void;
clearSearchHistory: () => void;
removeSearchHistoryItem: (query: string) => void;
updateSettings: (patch: Record<string, PhoneSettingsValue>) => void;
}
// ---- Initial state ----
const initialState: ContactsState = {
searchHistory: [],
settings: { ...(CONTACTS_CONFIG.settings ?? {}) },
};
// ---- Store ----
export const useContactsStore = createAppStoreWithActions<ContactsState, ContactsActions>(
'contacts',
initialState,
(set, get) => ({
addSearchHistory: (query) => {
const q = query.trim();
if (!q) return;
set((state) => {
const next = [q, ...state.searchHistory.filter((x) => x !== q)];
return { searchHistory: next.slice(0, 20) };
});
},
clearSearchHistory: () => {
set({ searchHistory: [] });
},
removeSearchHistoryItem: (query) => {
const q = query.trim();
if (!q) return;
set((state) => ({
searchHistory: state.searchHistory.filter((x) => x !== q),
}));
},
updateSettings: (patch) => {
set((state) => ({ settings: { ...state.settings, ...patch } }));
},
}),
);
// ---- Convenience hooks for phone settings ----
export function usePreferenceValue<T extends PhoneSettingsValue>(
key: string,
fallback: T,
): [T, (v: T) => void] {
const value = useContactsStore((s) => {
const val = s.settings[key];
return (val === undefined ? fallback : val) as T;
});
const updateSettings = useContactsStore((s) => s.updateSettings);
const setValue = useCallback(
(v: T) => updateSettings({ [key]: v }),
[key, updateSettings],
);
return [value, setValue];
}
export function useBooleanPreference(key: string, fallback: boolean): [boolean, (v: boolean) => void] {
const [v, setV] = usePreferenceValue<boolean>(key, fallback);
return [Boolean(v), (next) => setV(next)];
}
export function useStringPreference(key: string, fallback: string): [string, (v: string) => void] {
const [v, setV] = usePreferenceValue<string>(key, fallback);
return [String(v), (next) => setV(next)];
}
export function readPhoneSettingsPreference<T extends PhoneSettingsValue>(key: string): T | undefined {
const prefs = useContactsStore.getState().settings;
return prefs[key] as T | undefined;
}
function queryContacts(uri: string): Contact[] {
ensureContactsProviderRegistered();
try {
return ContentResolver.query<Contact>(uri).items;
} catch {
return [];
}
}
let _contactsSnapshot: Contact[] | null = null;
let _starredSnapshot: Contact[] | null = null;
function ensureContactsSnapshots() {
if (_contactsSnapshot && _starredSnapshot) return;
_contactsSnapshot = queryContacts('content://contacts/contacts');
_starredSnapshot = queryContacts('content://contacts/contacts/starred');
}
function refreshContactsSnapshots() {
_contactsSnapshot = queryContacts('content://contacts/contacts');
_starredSnapshot = queryContacts('content://contacts/contacts/starred');
}
function subscribeContacts(listener: () => void): () => void {
return ContentResolver.registerContentObserver('content://contacts/contacts', () => {
refreshContactsSnapshots();
listener();
});
}
export function useContactsList(): Contact[] {
return useSyncExternalStore(
subscribeContacts,
() => {
ensureContactsSnapshots();
return _contactsSnapshot ?? [];
},
() => [],
);
}
export function useContact(contactId?: string): Contact | undefined {
return useSyncExternalStore(
subscribeContacts,
() => {
if (!contactId) return undefined;
return queryContacts(`content://contacts/contacts/${contactId}`)[0];
},
() => {
if (!contactId) return undefined;
return queryContacts(`content://contacts/contacts/${contactId}`)[0];
},
);
}
export function createContact(draft: Omit<Contact, 'id' | 'createdAt' | 'updatedAt'>): ContactId {
const uri = ContentResolver.insert('content://contacts/contacts', {
...draft,
createdAt: TimeService.now(),
updatedAt: TimeService.now(),
});
return String(uri.split('/').pop() ?? '');
}
export function updateContact(contactId: ContactId, patch: Partial<Omit<Contact, 'id'>>): void {
ContentResolver.update(`content://contacts/contacts/${contactId}`, patch);
}
export function deleteContact(contactId: ContactId): void {
ContentResolver.delete(`content://contacts/contacts/${contactId}`);
}
export function toggleStarred(contactId: ContactId, to?: boolean): void {
const current = queryContacts(`content://contacts/contacts/${contactId}`)[0];
if (!current) return;
ContentResolver.update(`content://contacts/contacts/${contactId}`, {
starred: typeof to === 'boolean' ? to : !Boolean(current.starred),
});
}
export function recordLastContacted(contactId: ContactId): void {
const now = TimeService.now();
ContentResolver.update(`content://contacts/contacts/${contactId}`, {
lastContactedAt: now,
updatedAt: now,
});
}
export function useStarredContacts(): Contact[] {
return useSyncExternalStore(
subscribeContacts,
() => {
ensureContactsSnapshots();
return _starredSnapshot ?? [];
},
() => [],
);
}