diff --git a/app/repository/contacts.py b/app/repository/contacts.py index 3af13c7..b5e257b 100644 --- a/app/repository/contacts.py +++ b/app/repository/contacts.py @@ -215,6 +215,19 @@ class ContactRepository: ) await db.conn.commit() + @staticmethod + async def clear_on_radio_except(keep_keys: list[str]) -> None: + """Set on_radio=False for all contacts NOT in keep_keys.""" + if not keep_keys: + await db.conn.execute("UPDATE contacts SET on_radio = 0 WHERE on_radio = 1") + else: + placeholders = ",".join("?" * len(keep_keys)) + await db.conn.execute( + f"UPDATE contacts SET on_radio = 0 WHERE on_radio = 1 AND public_key NOT IN ({placeholders})", + keep_keys, + ) + await db.conn.commit() + @staticmethod async def delete(public_key: str) -> None: normalized = public_key.lower() diff --git a/app/routers/channels.py b/app/routers/channels.py index a14c428..1535d5e 100644 --- a/app/routers/channels.py +++ b/app/routers/channels.py @@ -127,4 +127,9 @@ async def delete_channel(key: str) -> dict: """ logger.info("Deleting channel %s from database", key) await ChannelRepository.delete(key) + + from app.websocket import broadcast_event + + broadcast_event("channel_deleted", {"key": key}) + return {"status": "ok"} diff --git a/app/routers/contacts.py b/app/routers/contacts.py index defb331..0d4ee03 100644 --- a/app/routers/contacts.py +++ b/app/routers/contacts.py @@ -271,16 +271,21 @@ async def sync_contacts_from_radio() -> dict: contacts = result.payload count = 0 + synced_keys: list[str] = [] for public_key, contact_data in contacts.items(): lower_key = public_key.lower() await ContactRepository.upsert( Contact.from_radio_dict(lower_key, contact_data, on_radio=True) ) + synced_keys.append(lower_key) claimed = await MessageRepository.claim_prefix_messages(lower_key) if claimed > 0: logger.info("Claimed %d prefix DM message(s) for contact %s", claimed, public_key[:12]) count += 1 + # Clear on_radio for contacts not found on the radio + await ContactRepository.clear_on_radio_except(synced_keys) + logger.info("Synced %d contacts from radio", count) return {"synced": count} @@ -368,6 +373,10 @@ async def delete_contact(public_key: str) -> dict: await ContactRepository.delete(contact.public_key) logger.info("Deleted contact %s", contact.public_key[:12]) + from app.websocket import broadcast_event + + broadcast_event("contact_deleted", {"public_key": contact.public_key}) + return {"status": "ok"} diff --git a/app/routers/messages.py b/app/routers/messages.py index 2e999bc..72bf7b2 100644 --- a/app/routers/messages.py +++ b/app/routers/messages.py @@ -399,6 +399,7 @@ async def resend_channel_message( sender_timestamp=now, received_at=now, outgoing=True, + sender_name=radio_name or None, ) if new_msg_id is None: # Timestamp-second collision (same text+channel within the same second). diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f470a3e..73ab1ab 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -287,6 +287,14 @@ export function App() { onContact: (contact: Contact) => { setContacts((prev) => mergeContactIntoList(prev, contact)); }, + onContactDeleted: (publicKey: string) => { + setContacts((prev) => prev.filter((c) => c.public_key !== publicKey)); + messageCache.remove(publicKey); + }, + onChannelDeleted: (key: string) => { + setChannels((prev) => prev.filter((c) => c.key !== key)); + messageCache.remove(key); + }, onRawPacket: (packet: RawPacket) => { setRawPackets((prev) => appendRawPacketUnique(prev, packet, MAX_RAW_PACKETS)); }, @@ -306,6 +314,7 @@ export function App() { setConfig, activeConversationRef, setContacts, + setChannels, triggerReconcile, refreshUnreads, fetchAllContacts, diff --git a/frontend/src/useWebSocket.ts b/frontend/src/useWebSocket.ts index c1d6505..5269468 100644 --- a/frontend/src/useWebSocket.ts +++ b/frontend/src/useWebSocket.ts @@ -20,6 +20,8 @@ interface UseWebSocketOptions { onHealth?: (health: HealthStatus) => void; onMessage?: (message: Message) => void; onContact?: (contact: Contact) => void; + onContactDeleted?: (publicKey: string) => void; + onChannelDeleted?: (key: string) => void; onRawPacket?: (packet: RawPacket) => void; onMessageAcked?: (messageId: number, ackCount: number, paths?: MessagePath[]) => void; onError?: (error: ErrorEvent) => void; @@ -103,6 +105,12 @@ export function useWebSocket(options: UseWebSocketOptions) { case 'contact': handlers.onContact?.(msg.data as Contact); break; + case 'contact_deleted': + handlers.onContactDeleted?.((msg.data as { public_key: string }).public_key); + break; + case 'channel_deleted': + handlers.onChannelDeleted?.((msg.data as { key: string }).key); + break; case 'raw_packet': handlers.onRawPacket?.(msg.data as RawPacket); break;