Patch up radio locking and frontend contact delete behavior for bulk contact delete

This commit is contained in:
Jack Kingsman
2026-04-01 16:52:25 -07:00
parent fd1188abcd
commit 630ba67ef0
5 changed files with 29 additions and 16 deletions
+15 -12
View File
@@ -357,24 +357,27 @@ async def bulk_delete_contacts(request: BulkDeleteRequest) -> dict:
"""Delete multiple contacts from the database (and radio if present).""" """Delete multiple contacts from the database (and radio if present)."""
from app.websocket import broadcast_event from app.websocket import broadcast_event
deleted = 0 # Resolve all contacts first
contacts_to_delete: list[Contact] = []
for key in request.public_keys: for key in request.public_keys:
normalized = key.lower() contact = await ContactRepository.get_by_key(key.lower())
contact = await ContactRepository.get_by_key(normalized) if contact:
if not contact: contacts_to_delete.append(contact)
continue
if radio_manager.is_connected: # Remove from radio in a single locked operation (blocks until radio is free)
try: if radio_manager.is_connected and contacts_to_delete:
async with radio_manager.radio_operation( try:
"bulk_delete_contact_from_radio", blocking=False async with radio_manager.radio_operation("bulk_delete_contacts_from_radio") as mc:
) as mc: for contact in contacts_to_delete:
radio_contact = mc.get_contact_by_key_prefix(contact.public_key[:12]) radio_contact = mc.get_contact_by_key_prefix(contact.public_key[:12])
if radio_contact: if radio_contact:
await mc.commands.remove_contact(radio_contact) await mc.commands.remove_contact(radio_contact)
except Exception: except Exception as e:
pass # Best-effort radio removal during bulk delete logger.warning("Radio removal during bulk delete failed: %s", e)
# Delete from database and broadcast events
deleted = 0
for contact in contacts_to_delete:
await ContactRepository.delete(contact.public_key) await ContactRepository.delete(contact.public_key)
broadcast_event("contact_deleted", {"public_key": contact.public_key}) broadcast_event("contact_deleted", {"public_key": contact.public_key})
deleted += 1 deleted += 1
+4
View File
@@ -558,6 +558,10 @@ export function App() {
onToggleBlockedKey: handleBlockKey, onToggleBlockedKey: handleBlockKey,
onToggleBlockedName: handleBlockName, onToggleBlockedName: handleBlockName,
contacts, contacts,
onBulkDeleteContacts: (deletedKeys: string[]) => {
const keySet = new Set(deletedKeys.map((k) => k.toLowerCase()));
setContacts((prev) => prev.filter((c) => !keySet.has(c.public_key.toLowerCase())));
},
}; };
const crackerProps = { const crackerProps = {
packets: rawPackets, packets: rawPackets,
@@ -49,6 +49,7 @@ interface SettingsModalBaseProps {
onToggleBlockedKey?: (key: string) => void; onToggleBlockedKey?: (key: string) => void;
onToggleBlockedName?: (name: string) => void; onToggleBlockedName?: (name: string) => void;
contacts?: Contact[]; contacts?: Contact[];
onBulkDeleteContacts?: (deletedKeys: string[]) => void;
} }
export type SettingsModalProps = SettingsModalBaseProps & export type SettingsModalProps = SettingsModalBaseProps &
@@ -83,6 +84,7 @@ export function SettingsModal(props: SettingsModalProps) {
onToggleBlockedKey, onToggleBlockedKey,
onToggleBlockedName, onToggleBlockedName,
contacts, contacts,
onBulkDeleteContacts,
} = props; } = props;
const externalSidebarNav = props.externalSidebarNav === true; const externalSidebarNav = props.externalSidebarNav === true;
const desktopSection = props.externalSidebarNav ? props.desktopSection : undefined; const desktopSection = props.externalSidebarNav ? props.desktopSection : undefined;
@@ -243,6 +245,7 @@ export function SettingsModal(props: SettingsModalProps) {
onToggleBlockedKey={onToggleBlockedKey} onToggleBlockedKey={onToggleBlockedKey}
onToggleBlockedName={onToggleBlockedName} onToggleBlockedName={onToggleBlockedName}
contacts={contacts} contacts={contacts}
onBulkDeleteContacts={onBulkDeleteContacts}
className={sectionContentClass} className={sectionContentClass}
/> />
) : ( ) : (
@@ -36,7 +36,7 @@ interface BulkDeleteContactsModalProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
contacts: Contact[]; contacts: Contact[];
onDeleted: () => void; onDeleted: (deletedKeys: string[]) => void;
} }
export function BulkDeleteContactsModal({ export function BulkDeleteContactsModal({
@@ -133,9 +133,10 @@ export function BulkDeleteContactsModal({
const handleDelete = async () => { const handleDelete = async () => {
setDeleting(true); setDeleting(true);
try { try {
const result = await api.bulkDeleteContacts([...selectedKeys]); const keysToDelete = [...selectedKeys];
const result = await api.bulkDeleteContacts(keysToDelete);
toast.success(`Deleted ${result.deleted} contact${result.deleted === 1 ? '' : 's'}`); toast.success(`Deleted ${result.deleted} contact${result.deleted === 1 ? '' : 's'}`);
onDeleted(); onDeleted(keysToDelete);
resetAndClose(); resetAndClose();
} catch (err) { } catch (err) {
console.error('Bulk delete failed:', err); console.error('Bulk delete failed:', err);
@@ -19,6 +19,7 @@ export function SettingsDatabaseSection({
onToggleBlockedKey, onToggleBlockedKey,
onToggleBlockedName, onToggleBlockedName,
contacts = [], contacts = [],
onBulkDeleteContacts,
className, className,
}: { }: {
appSettings: AppSettings; appSettings: AppSettings;
@@ -30,6 +31,7 @@ export function SettingsDatabaseSection({
onToggleBlockedKey?: (key: string) => void; onToggleBlockedKey?: (key: string) => void;
onToggleBlockedName?: (name: string) => void; onToggleBlockedName?: (name: string) => void;
contacts?: Contact[]; contacts?: Contact[];
onBulkDeleteContacts?: (deletedKeys: string[]) => void;
className?: string; className?: string;
}) { }) {
const [retentionDays, setRetentionDays] = useState('14'); const [retentionDays, setRetentionDays] = useState('14');
@@ -297,7 +299,7 @@ export function SettingsDatabaseSection({
open={bulkDeleteOpen} open={bulkDeleteOpen}
onClose={() => setBulkDeleteOpen(false)} onClose={() => setBulkDeleteOpen(false)}
contacts={contacts} contacts={contacts}
onDeleted={() => {}} onDeleted={(keys) => onBulkDeleteContacts?.(keys)}
/> />
</div> </div>