feat: wire delete_message, update_profile, add_contact into frontend UI
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user