diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ef5d469..5c3cb797 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix: Display "unconfirmed" label for contacts that have not yet joined their private conversation. - Fix: In public converastion invitations, use the cell name as the conversation title when convesation config is not available. - Fix: Open external links in message content in system default browser or mail client. +- Feat: Delete a contact by clicking the "Delete Contact" button on their page. ## [0.7.5] - 2025-01-10 diff --git a/ui/src/lib/Dialog.svelte b/ui/src/lib/Dialog.svelte new file mode 100644 index 00000000..ec022675 --- /dev/null +++ b/ui/src/lib/Dialog.svelte @@ -0,0 +1,58 @@ + + +{#if open} +
+
e.key === "Enter" && handleCancel()} + /> +
+

{title}

+
+ +
+
+ + +
+
+
+{/if} diff --git a/ui/src/lib/svgIcons.ts b/ui/src/lib/svgIcons.ts index d0e6aa6c..adc5c4ac 100644 --- a/ui/src/lib/svgIcons.ts +++ b/ui/src/lib/svgIcons.ts @@ -7,7 +7,8 @@ * Before adding an svg string here you MUST complete the following steps: * 1. Ensure the svg string does NOT contain the properties: "fill", "stroke", "width", or "height" * If it does, delete them. - * 2. Add the following properties to the svg element: fill="currentColor" width="100%" height="100%" + * 2. For fill based icons, add the following properties to the svg element: fill="currentColor" width="100%" height="100%" + * 3. For stroke based icons, add the following properties to the svg element: stroke="currentColor" width="100%" height="100%" */ export const svgIcons: { [key: string]: string } = { @@ -90,4 +91,5 @@ export const svgIcons: { [key: string]: string } = { pdf: '', fileClip: '', + delete: '', }; diff --git a/ui/src/routes/contacts/[id]/+page.svelte b/ui/src/routes/contacts/[id]/+page.svelte index d8673280..d454d03c 100644 --- a/ui/src/routes/contacts/[id]/+page.svelte +++ b/ui/src/routes/contacts/[id]/+page.svelte @@ -3,10 +3,8 @@ import { page } from "$app/stores"; import Button from "$lib/Button.svelte"; import ButtonIconBare from "$lib/ButtonIconBare.svelte"; - import ButtonsCopyShare from "$lib/ButtonsCopyShare.svelte"; import ButtonsCopyShareIcon from "$lib/ButtonsCopyShareIcon.svelte"; import Header from "$lib/Header.svelte"; - import SvgIcon from "$lib/SvgIcon.svelte"; import { deriveAgentContactStore, type ContactStore } from "$store/ContactStore"; import { getContext, onDestroy, onMount } from "svelte"; import { t } from "$translations"; @@ -21,6 +19,8 @@ import type { AgentPubKeyB64 } from "@holochain/client"; import { POLLING_INTERVAL_SLOW } from "$config"; import NoticeContactNotJoined from "$lib/NoticeContactNotJoined.svelte"; + import Dialog from "$lib/Dialog.svelte"; + import toast from "svelte-french-toast"; const contactStore = getContext<{ getStore: () => ContactStore }>("contactStore").getStore(); const conversationStore = getContext<{ getStore: () => ConversationStore }>( @@ -33,7 +33,8 @@ const myPubKeyB64 = getContext<{ getMyPubKeyB64: () => AgentPubKeyB64 }>( "myPubKey", ).getMyPubKeyB64(); - $: myProfile = $provisionedRelayCellProfileStore.data[myPubKeyB64]; + + let isDeletingContact = false; let contact = deriveAgentContactStore(contactStore, $page.params.id); let conversation = @@ -45,9 +46,12 @@ ? deriveCellProfileStore(profileStore, encodeCellIdToBase64($contact.cellId)) : undefined; + let showDeleteDialog = false; + let pollInterval: NodeJS.Timeout; $: hasAgentJoinedDht = profiles !== undefined && + $contact !== undefined && $profiles?.list.find(([key]) => key === $contact.publicKeyB64) !== undefined; async function loadProfiles() { @@ -61,57 +65,92 @@ } } + async function handleDeleteContact() { + if (isDeletingContact) return; + + isDeletingContact = true; + try { + await contact.delete(); + toast.success($t("common.delete_contact_success")); + await goto("/create"); + showDeleteDialog = false; + } catch (err) { + console.error("Error deleting contact:", err); + toast.error($t("common.delete_contact_error")); + } + isDeletingContact = false; + } + onMount(() => { loadProfiles(); }); - onDestroy(() => clearInterval(pollInterval)); + + onDestroy(() => { + clearInterval(pollInterval); + });
-
-
- +{#if $contact} +
+
+ -
-

{$contact.fullName}

+
+

{$contact.fullName}

- goto(`/contacts/${$page.params.id}/edit`)} - icon="write" - iconColor="gray" - /> -
-
- - {$contact.publicKeyB64} - - + goto(`/contacts/${$page.params.id}/edit`)} + icon="write" + iconColor="gray" + /> +
+
+ + {$contact.publicKeyB64} + + +
-
-
- {#if $contact.cellId !== undefined && conversation !== undefined} - {#if hasAgentJoinedDht} -
- -
- {:else} - +
+ {#if $contact.cellId !== undefined && conversation !== undefined} + {#if hasAgentJoinedDht} +
+ +
+ {:else} + + {/if} {/if} - {/if} + +
-
+{/if} + + +

{$t("common.delete_contact_dialog_message")}

+
diff --git a/ui/src/store/ContactStore.ts b/ui/src/store/ContactStore.ts index ae324685..669b27e3 100644 --- a/ui/src/store/ContactStore.ts +++ b/ui/src/store/ContactStore.ts @@ -20,6 +20,7 @@ export interface ContactStore { initialize: () => Promise; create: (val: Contact, cellIdB64: CellIdB64) => Promise; update: (key: AgentPubKeyB64, val: Contact) => Promise; + delete: (agentPubKeyB64: AgentPubKeyB64) => Promise; subscribe: ( this: void, run: Subscriber>, @@ -76,6 +77,20 @@ export function createContactStore(client: RelayClient): ContactStore { ); } + /** + * Delete a contact + */ + async function deleteContact(agentPubKeyB64: AgentPubKeyB64) { + const contact = contacts.getKeyValue(agentPubKeyB64); + await client.deleteContact(contact.originalActionHash); + cellIds.update((d) => { + const updated = { ...d }; + delete updated[agentPubKeyB64]; + return updated; + }); + contacts.removeKeyValue(agentPubKeyB64); + } + /** * Fetch contacts data and load into writable */ @@ -157,6 +172,7 @@ export function createContactStore(client: RelayClient): ContactStore { initialize, create, update, + delete: deleteContact, subscribe: contacts.subscribe, }; } @@ -179,6 +195,7 @@ export function deriveAgentContactStore( return { update: (val: Contact) => contactStore.update(agentPubKeyB64, val), + delete: () => contactStore.delete(agentPubKeyB64), subscribe, }; } diff --git a/ui/src/store/RelayClient.ts b/ui/src/store/RelayClient.ts index 366030d3..37b006bc 100644 --- a/ui/src/store/RelayClient.ts +++ b/ui/src/store/RelayClient.ts @@ -332,4 +332,13 @@ export class RelayClient { payload, }); } + + public async deleteContact(originalContactHash: ActionHash): Promise { + return this.client.callZome({ + cell_id: this.provisionedRelayCellId, + zome_name: ZOME_NAME, + fn_name: "delete_contact", + payload: originalContactHash, + }); + } } diff --git a/ui/src/translations/locales/en.json b/ui/src/translations/locales/en.json index 294f763b..00ae737e 100644 --- a/ui/src/translations/locales/en.json +++ b/ui/src/translations/locales/en.json @@ -12,6 +12,7 @@ "connecting_to_holochain": "Connecting to Holochain...", "contact_already_exist": "Contact already exists", "contact_code": "Contact code", + "confirm": "Confirm", "copy": "Copy", "copy_error": "Failed to copy to clipboard", "copy_invite": "Copy Invite", @@ -26,6 +27,11 @@ "create_group": "Create group", "create_new_contact": "Create New Contact", "created": "Created: {{date:date;}}", + "delete": "Delete", + "delete_contact": "Delete Contact", + "delete_contact_error": "Error deleting contact", + "delete_contact_success": "Contact deleted successfully", + "delete_contact_dialog_message": "Are you sure you want to delete this contact? This action cannot be undone.", "download": "Download", "download_file_error": "Failed to download file", "download_file_success": "File downloaded successfully", @@ -90,4 +96,4 @@ "welcome_text_2": "Private, encrypted and secured by keys only you control.", "what_is_your_name": "What is your name?", "you": "You" -} +} \ No newline at end of file