feat: wire delete_message, update_profile, add_contact into frontend UI

This commit is contained in:
2026-04-18 01:29:36 -04:00
parent fbe37d8310
commit 47ec72defd
3 changed files with 137 additions and 6 deletions

View File

@@ -1,23 +1,27 @@
<script lang="ts"> <script lang="ts">
import { tick } from 'svelte'; import { tick } from 'svelte';
import type { Room, Message, ContextMenuItem } from '$lib/types'; import type { User, Room, Message, ContextMenuItem } from '$lib/types';
import { full, sid, fmt } from '$lib/helpers'; import { full, sid, fmt } from '$lib/helpers';
interface Props { interface Props {
activeRoom: Room | null; activeRoom: Room | null;
messages: Message[]; messages: Message[];
user: User | null;
err: string; err: string;
fMsg: string; fMsg: string;
onSendMessage: () => void; onSendMessage: () => void;
onDeleteMessage: (msgId: string) => void;
onShowMenu: (e: MouseEvent, items: ContextMenuItem[]) => void; onShowMenu: (e: MouseEvent, items: ContextMenuItem[]) => void;
} }
let { let {
activeRoom, activeRoom,
messages, messages,
user,
err, err,
fMsg = $bindable(), fMsg = $bindable(),
onSendMessage, onSendMessage,
onDeleteMessage,
onShowMenu, onShowMenu,
}: Props = $props(); }: Props = $props();
@@ -81,7 +85,15 @@
<div <div
class="msg" class="msg"
class:grouped={isGrouped(i)} class:grouped={isGrouped(i)}
oncontextmenu={(e) => onShowMenu(e, [{ label: 'Copy message', action: () => navigator.clipboard.writeText(msg.body) }])} oncontextmenu={(e) => {
const items: ContextMenuItem[] = [
{ label: 'Copy message', action: () => navigator.clipboard.writeText(msg.body) },
];
if (user && full(msg.author) === full(user.id)) {
items.push({ label: 'Delete message', action: () => onDeleteMessage(full(msg.id)) });
}
onShowMenu(e, items);
}}
> >
{#if !isGrouped(i)} {#if !isGrouped(i)}
<div class="msg-header"> <div class="msg-header">

View File

@@ -13,6 +13,8 @@
onCreateRoom: () => void; onCreateRoom: () => void;
onSignout: () => void; onSignout: () => void;
onShowMenu: (e: MouseEvent, items: ContextMenuItem[]) => void; onShowMenu: (e: MouseEvent, items: ContextMenuItem[]) => void;
onUpdateProfile: (fields: { username?: string; avatar?: string }) => Promise<void>;
onAddContact: (userId: string) => Promise<void>;
} }
let { let {
@@ -26,7 +28,48 @@
onCreateRoom, onCreateRoom,
onSignout, onSignout,
onShowMenu, onShowMenu,
onUpdateProfile,
onAddContact,
}: Props = $props(); }: Props = $props();
// ── Profile edit ──────────────────────────────────────────────────────────
let showEditProfile = $state(false);
let fProfileUsername = $state('');
let fProfileAvatar = $state('');
let profileErr = $state('');
function openEditProfile() {
fProfileUsername = user?.username ?? '';
fProfileAvatar = user?.avatar ?? '';
profileErr = '';
showEditProfile = true;
}
async function submitProfile() {
profileErr = '';
try {
await onUpdateProfile({
username: fProfileUsername.trim() || undefined,
avatar: fProfileAvatar.trim() || undefined,
});
showEditProfile = false;
} catch (e) { profileErr = String(e); }
}
// ── Add contact ───────────────────────────────────────────────────────────
let showAddContact = $state(false);
let fContactId = $state('');
let contactErr = $state('');
async function submitContact() {
if (!fContactId.trim()) return;
contactErr = '';
try {
await onAddContact(fContactId.trim());
fContactId = '';
showAddContact = false;
} catch (e) { contactErr = String(e); }
}
</script> </script>
<aside class="sidebar"> <aside class="sidebar">
@@ -68,8 +111,21 @@
</nav> </nav>
<!-- Contacts --> <!-- Contacts -->
<div class="section-label-row">
<span class="section-label">CONTACTS</span>
<button class="icon-btn" title="Add contact" onclick={() => { showAddContact = !showAddContact; }}>
{showAddContact ? '×' : '+'}
</button>
</div>
{#if showAddContact}
<div class="new-room-form">
<input class="field-sm" placeholder="user id" bind:value={fContactId}
onkeydown={(e) => e.key === 'Enter' && submitContact()} />
<button class="btn-xs" onclick={submitContact}>add</button>
</div>
{#if contactErr}<p class="form-err" style="padding: 0 12px 6px">{contactErr}</p>{/if}
{/if}
{#if contacts.length > 0} {#if contacts.length > 0}
<div class="section-label">CONTACTS</div>
<div class="contact-list"> <div class="contact-list">
{#each contacts as c} {#each contacts as c}
<div class="contact-item"> <div class="contact-item">
@@ -81,11 +137,26 @@
{/if} {/if}
<!-- User footer --> <!-- User footer -->
{#if showEditProfile}
<div class="edit-profile-form">
<input class="field-sm" placeholder="username" bind:value={fProfileUsername}
onkeydown={(e) => e.key === 'Enter' && submitProfile()}
onkeyup={(e) => e.key === 'Escape' && (showEditProfile = false)} />
<input class="field-sm" placeholder="avatar url (optional)" bind:value={fProfileAvatar}
onkeydown={(e) => e.key === 'Enter' && submitProfile()}
onkeyup={(e) => e.key === 'Escape' && (showEditProfile = false)} />
{#if profileErr}<p class="form-err">{profileErr}</p>{/if}
<div class="form-row">
<button class="btn-xs" onclick={submitProfile}>save</button>
<button class="btn-xs btn-ghost" onclick={() => showEditProfile = false}>cancel</button>
</div>
</div>
{/if}
<div class="user-footer"> <div class="user-footer">
<div class="user-pill"> <button class="user-pill" title="Edit profile" onclick={openEditProfile}>
<span class="avatar">{user?.username?.[0]?.toUpperCase() ?? '?'}</span> <span class="avatar">{user?.username?.[0]?.toUpperCase() ?? '?'}</span>
<span class="user-name">{user?.username ?? ''}</span> <span class="user-name">{user?.username ?? ''}</span>
</div> </button>
<button class="icon-btn signout" title="Sign out" onclick={onSignout}> <button class="icon-btn signout" title="Sign out" onclick={onSignout}>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/> <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
@@ -202,7 +273,34 @@
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
background: var(--surface); margin-top: auto; background: var(--surface); margin-top: auto;
} }
.user-pill { display: flex; align-items: center; gap: 8px; min-width: 0; } .user-pill {
display: flex; align-items: center; gap: 8px; min-width: 0;
background: none; border: none; cursor: pointer; padding: 0;
font-family: inherit; text-align: left;
}
.user-pill:hover .user-name { color: var(--text); }
.section-label-row {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 14px 5px 14px;
}
.section-label-row .section-label { padding: 0; }
.edit-profile-form {
display: flex; flex-direction: column; gap: 6px;
padding: 10px 12px;
border-top: 1px solid var(--border-subtle);
animation: rise 0.15s ease;
}
.form-row { display: flex; gap: 6px; }
.btn-ghost {
background: transparent; border: 1px solid var(--border); color: var(--muted);
}
.btn-ghost:hover { opacity: 0.8; border-color: var(--muted); }
.form-err {
font-size: 10px; color: var(--danger);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.avatar { .avatar {
width: 26px; height: 26px; flex-shrink: 0; width: 26px; height: 26px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;

View File

@@ -117,6 +117,23 @@
} catch (e) { err = String(e); } } catch (e) { err = String(e); }
} }
async function deleteMessage(msgId: string) {
err = '';
try {
await cmd('delete_message', { messageId: msgId });
messages = messages.filter(m => full(m.id) !== msgId);
} catch (e) { err = String(e); }
}
async function updateProfile(fields: { username?: string; avatar?: string }) {
user = await cmd<User>('update_profile', fields);
}
async function addContact(userId: string) {
await cmd('add_contact', { userId });
contacts = await cmd<User[]>('get_contacts').catch(() => []);
}
onMount(init); onMount(init);
onDestroy(async () => { onDestroy(async () => {
if (subId) await cmd('unsubscribe_room', { subId }).catch(() => {}); if (subId) await cmd('unsubscribe_room', { subId }).catch(() => {});
@@ -152,13 +169,17 @@
onCreateRoom={createRoom} onCreateRoom={createRoom}
onSignout={signout} onSignout={signout}
onShowMenu={showMenu} onShowMenu={showMenu}
onUpdateProfile={updateProfile}
onAddContact={addContact}
/> />
<ChatMain <ChatMain
{activeRoom} {activeRoom}
{messages} {messages}
{user}
{err} {err}
bind:fMsg bind:fMsg
onSendMessage={sendMessage} onSendMessage={sendMessage}
onDeleteMessage={deleteMessage}
onShowMenu={showMenu} onShowMenu={showMenu}
/> />
</div> </div>