Adjust phrasing on new-chat modal, and remove the unusable existing-contact scren. Closes #105.

This commit is contained in:
jkingsman
2026-03-24 10:00:29 -07:00
parent 5225a1c766
commit b022aea71f
4 changed files with 20 additions and 98 deletions
-2
View File
@@ -506,9 +506,7 @@ export function App() {
onChannelCreate: handleCreateCrackedChannel, onChannelCreate: handleCreateCrackedChannel,
}; };
const newMessageModalProps = { const newMessageModalProps = {
contacts,
undecryptedCount, undecryptedCount,
onSelectConversation: handleSelectConversationWithTargetReset,
onCreateContact: handleCreateContact, onCreateContact: handleCreateContact,
onCreateChannel: handleCreateChannel, onCreateChannel: handleCreateChannel,
onCreateHashtagChannel: handleCreateHashtagChannel, onCreateHashtagChannel: handleCreateHashtagChannel,
-4
View File
@@ -284,10 +284,6 @@ export function AppShell({
{...newMessageModalProps} {...newMessageModalProps}
open={showNewMessage} open={showNewMessage}
onClose={onCloseNewMessage} onClose={onCloseNewMessage}
onSelectConversation={(conv) => {
newMessageModalProps.onSelectConversation(conv);
onCloseNewMessage();
}}
/> />
<SecurityWarningModal health={statusProps.health} /> <SecurityWarningModal health={statusProps.health} />
+9 -59
View File
@@ -1,7 +1,5 @@
import { useState, useRef } from 'react'; import { useState, useRef } from 'react';
import { Dice5 } from 'lucide-react'; import { Dice5 } from 'lucide-react';
import type { Contact, Conversation } from '../types';
import { getContactDisplayName } from '../utils/pubkey';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -17,14 +15,12 @@ import { Checkbox } from './ui/checkbox';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { toast } from './ui/sonner'; import { toast } from './ui/sonner';
type Tab = 'existing' | 'new-contact' | 'new-room' | 'hashtag'; type Tab = 'new-contact' | 'new-room' | 'hashtag';
interface NewMessageModalProps { interface NewMessageModalProps {
open: boolean; open: boolean;
contacts: Contact[];
undecryptedCount: number; undecryptedCount: number;
onClose: () => void; onClose: () => void;
onSelectConversation: (conversation: Conversation) => void;
onCreateContact: (name: string, publicKey: string, tryHistorical: boolean) => Promise<void>; onCreateContact: (name: string, publicKey: string, tryHistorical: boolean) => Promise<void>;
onCreateChannel: (name: string, key: string, tryHistorical: boolean) => Promise<void>; onCreateChannel: (name: string, key: string, tryHistorical: boolean) => Promise<void>;
onCreateHashtagChannel: (name: string, tryHistorical: boolean) => Promise<void>; onCreateHashtagChannel: (name: string, tryHistorical: boolean) => Promise<void>;
@@ -32,15 +28,13 @@ interface NewMessageModalProps {
export function NewMessageModal({ export function NewMessageModal({
open, open,
contacts,
undecryptedCount, undecryptedCount,
onClose, onClose,
onSelectConversation,
onCreateContact, onCreateContact,
onCreateChannel, onCreateChannel,
onCreateHashtagChannel, onCreateHashtagChannel,
}: NewMessageModalProps) { }: NewMessageModalProps) {
const [tab, setTab] = useState<Tab>('existing'); const [tab, setTab] = useState<Tab>('new-contact');
const [name, setName] = useState(''); const [name, setName] = useState('');
const [contactKey, setContactKey] = useState(''); const [contactKey, setContactKey] = useState('');
const [roomKey, setRoomKey] = useState(''); const [roomKey, setRoomKey] = useState('');
@@ -136,7 +130,7 @@ export function NewMessageModal({
} }
}; };
const showHistoricalOption = tab !== 'existing' && undecryptedCount > 0; const showHistoricalOption = undecryptedCount > 0;
return ( return (
<Dialog <Dialog
@@ -152,7 +146,6 @@ export function NewMessageModal({
<DialogHeader> <DialogHeader>
<DialogTitle>New Conversation</DialogTitle> <DialogTitle>New Conversation</DialogTitle>
<DialogDescription className="sr-only"> <DialogDescription className="sr-only">
{tab === 'existing' && 'Select an existing contact to start a conversation'}
{tab === 'new-contact' && 'Add a new contact by entering their name and public key'} {tab === 'new-contact' && 'Add a new contact by entering their name and public key'}
{tab === 'new-room' && 'Create a private room with a shared encryption key'} {tab === 'new-room' && 'Create a private room with a shared encryption key'}
{tab === 'hashtag' && 'Join a public hashtag channel'} {tab === 'hashtag' && 'Join a public hashtag channel'}
@@ -167,53 +160,12 @@ export function NewMessageModal({
}} }}
className="w-full" className="w-full"
> >
<TabsList className="grid w-full grid-cols-4"> <TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="existing">Existing</TabsTrigger>
<TabsTrigger value="new-contact">Contact</TabsTrigger> <TabsTrigger value="new-contact">Contact</TabsTrigger>
<TabsTrigger value="new-room">Room</TabsTrigger> <TabsTrigger value="new-room">Private Channel</TabsTrigger>
<TabsTrigger value="hashtag">Hashtag</TabsTrigger> <TabsTrigger value="hashtag">Hashtag Channel</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="existing" className="mt-4">
<div className="max-h-[300px] overflow-y-auto rounded-md border">
{contacts.filter((contact) => contact.public_key.length === 64).length === 0 ? (
<div className="p-4 text-center text-muted-foreground">No contacts available</div>
) : (
contacts
.filter((contact) => contact.public_key.length === 64)
.map((contact) => (
<div
key={contact.public_key}
className="cursor-pointer px-4 py-2 hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
(e.currentTarget as HTMLElement).click();
}
}}
onClick={() => {
onSelectConversation({
type: 'contact',
id: contact.public_key,
name: getContactDisplayName(
contact.name,
contact.public_key,
contact.last_advert
),
});
resetForm();
onClose();
}}
>
{getContactDisplayName(contact.name, contact.public_key, contact.last_advert)}
</div>
))
)}
</div>
</TabsContent>
<TabsContent value="new-contact" className="mt-4 space-y-4"> <TabsContent value="new-contact" className="mt-4 space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="contact-name">Name</Label> <Label htmlFor="contact-name">Name</Label>
@@ -353,11 +305,9 @@ export function NewMessageModal({
{loading ? 'Creating...' : 'Create & Add Another'} {loading ? 'Creating...' : 'Create & Add Another'}
</Button> </Button>
)} )}
{tab !== 'existing' && ( <Button onClick={handleCreate} disabled={loading}>
<Button onClick={handleCreate} disabled={loading}> {loading ? 'Creating...' : 'Create'}
{loading ? 'Creating...' : 'Create'} </Button>
</Button>
)}
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
+11 -33
View File
@@ -10,7 +10,6 @@ import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { NewMessageModal } from '../components/NewMessageModal'; import { NewMessageModal } from '../components/NewMessageModal';
import type { Contact } from '../types';
import { toast } from '../components/ui/sonner'; import { toast } from '../components/ui/sonner';
// Mock sonner (toast) // Mock sonner (toast)
@@ -18,24 +17,6 @@ vi.mock('../components/ui/sonner', () => ({
toast: { success: vi.fn(), error: vi.fn() }, toast: { success: vi.fn(), error: vi.fn() },
})); }));
const mockContact: Contact = {
public_key: 'aa'.repeat(32),
name: 'Alice',
type: 1,
flags: 0,
direct_path: null,
direct_path_len: -1,
direct_path_hash_mode: 0,
last_advert: null,
lat: null,
lon: null,
last_seen: null,
on_radio: false,
last_contacted: null,
last_read_at: null,
first_seen: null,
};
const mockToast = toast as unknown as { const mockToast = toast as unknown as {
success: ReturnType<typeof vi.fn>; success: ReturnType<typeof vi.fn>;
error: ReturnType<typeof vi.fn>; error: ReturnType<typeof vi.fn>;
@@ -43,7 +24,6 @@ const mockToast = toast as unknown as {
describe('NewMessageModal form reset', () => { describe('NewMessageModal form reset', () => {
const onClose = vi.fn(); const onClose = vi.fn();
const onSelectConversation = vi.fn();
const onCreateContact = vi.fn().mockResolvedValue(undefined); const onCreateContact = vi.fn().mockResolvedValue(undefined);
const onCreateChannel = vi.fn().mockResolvedValue(undefined); const onCreateChannel = vi.fn().mockResolvedValue(undefined);
const onCreateHashtagChannel = vi.fn().mockResolvedValue(undefined); const onCreateHashtagChannel = vi.fn().mockResolvedValue(undefined);
@@ -56,10 +36,8 @@ describe('NewMessageModal form reset', () => {
return render( return render(
<NewMessageModal <NewMessageModal
open={open} open={open}
contacts={[mockContact]}
undecryptedCount={5} undecryptedCount={5}
onClose={onClose} onClose={onClose}
onSelectConversation={onSelectConversation}
onCreateContact={onCreateContact} onCreateContact={onCreateContact}
onCreateChannel={onCreateChannel} onCreateChannel={onCreateChannel}
onCreateHashtagChannel={onCreateHashtagChannel} onCreateHashtagChannel={onCreateHashtagChannel}
@@ -75,7 +53,7 @@ describe('NewMessageModal form reset', () => {
it('clears name after successful Create', async () => { it('clears name after successful Create', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const { unmount } = renderModal(); const { unmount } = renderModal();
await switchToTab(user, 'Hashtag'); await switchToTab(user, 'Hashtag Channel');
const input = screen.getByPlaceholderText('channel-name') as HTMLInputElement; const input = screen.getByPlaceholderText('channel-name') as HTMLInputElement;
await user.type(input, 'testchan'); await user.type(input, 'testchan');
@@ -91,14 +69,14 @@ describe('NewMessageModal form reset', () => {
// Re-render to simulate reopening — state should be reset // Re-render to simulate reopening — state should be reset
renderModal(); renderModal();
await switchToTab(user, 'Hashtag'); await switchToTab(user, 'Hashtag Channel');
expect((screen.getByPlaceholderText('channel-name') as HTMLInputElement).value).toBe(''); expect((screen.getByPlaceholderText('channel-name') as HTMLInputElement).value).toBe('');
}); });
it('clears name when Cancel is clicked', async () => { it('clears name when Cancel is clicked', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
renderModal(); renderModal();
await switchToTab(user, 'Hashtag'); await switchToTab(user, 'Hashtag Channel');
const input = screen.getByPlaceholderText('channel-name') as HTMLInputElement; const input = screen.getByPlaceholderText('channel-name') as HTMLInputElement;
await user.type(input, 'mychannel'); await user.type(input, 'mychannel');
@@ -131,7 +109,7 @@ describe('NewMessageModal form reset', () => {
it('clears name and key after successful Create', async () => { it('clears name and key after successful Create', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
renderModal(); renderModal();
await switchToTab(user, 'Room'); await switchToTab(user, 'Private Channel');
await user.type(screen.getByPlaceholderText('Room name'), 'MyRoom'); await user.type(screen.getByPlaceholderText('Room name'), 'MyRoom');
await user.type(screen.getByPlaceholderText('Pre-shared key (hex)'), 'cc'.repeat(16)); await user.type(screen.getByPlaceholderText('Pre-shared key (hex)'), 'cc'.repeat(16));
@@ -148,7 +126,7 @@ describe('NewMessageModal form reset', () => {
const user = userEvent.setup(); const user = userEvent.setup();
onCreateChannel.mockRejectedValueOnce(new Error('Bad key')); onCreateChannel.mockRejectedValueOnce(new Error('Bad key'));
renderModal(); renderModal();
await switchToTab(user, 'Room'); await switchToTab(user, 'Private Channel');
await user.type(screen.getByPlaceholderText('Room name'), 'MyRoom'); await user.type(screen.getByPlaceholderText('Room name'), 'MyRoom');
await user.type(screen.getByPlaceholderText('Pre-shared key (hex)'), 'cc'.repeat(16)); await user.type(screen.getByPlaceholderText('Pre-shared key (hex)'), 'cc'.repeat(16));
@@ -172,8 +150,8 @@ describe('NewMessageModal form reset', () => {
await user.type(screen.getByPlaceholderText('Contact name'), 'Bob'); await user.type(screen.getByPlaceholderText('Contact name'), 'Bob');
await user.type(screen.getByPlaceholderText('64-character hex public key'), 'deadbeef'); await user.type(screen.getByPlaceholderText('64-character hex public key'), 'deadbeef');
// Switch to Room tab — fields should reset // Switch to Private Channel tab — fields should reset
await switchToTab(user, 'Room'); await switchToTab(user, 'Private Channel');
expect((screen.getByPlaceholderText('Room name') as HTMLInputElement).value).toBe(''); expect((screen.getByPlaceholderText('Room name') as HTMLInputElement).value).toBe('');
expect((screen.getByPlaceholderText('Pre-shared key (hex)') as HTMLInputElement).value).toBe( expect((screen.getByPlaceholderText('Pre-shared key (hex)') as HTMLInputElement).value).toBe(
@@ -184,12 +162,12 @@ describe('NewMessageModal form reset', () => {
it('clears room fields when switching to hashtag tab', async () => { it('clears room fields when switching to hashtag tab', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
renderModal(); renderModal();
await switchToTab(user, 'Room'); await switchToTab(user, 'Private Channel');
await user.type(screen.getByPlaceholderText('Room name'), 'SecretRoom'); await user.type(screen.getByPlaceholderText('Room name'), 'SecretRoom');
await user.type(screen.getByPlaceholderText('Pre-shared key (hex)'), 'ff'.repeat(16)); await user.type(screen.getByPlaceholderText('Pre-shared key (hex)'), 'ff'.repeat(16));
await switchToTab(user, 'Hashtag'); await switchToTab(user, 'Hashtag Channel');
expect((screen.getByPlaceholderText('channel-name') as HTMLInputElement).value).toBe(''); expect((screen.getByPlaceholderText('channel-name') as HTMLInputElement).value).toBe('');
}); });
@@ -199,7 +177,7 @@ describe('NewMessageModal form reset', () => {
it('resets tryHistorical when switching tabs', async () => { it('resets tryHistorical when switching tabs', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
renderModal(); renderModal();
await switchToTab(user, 'Hashtag'); await switchToTab(user, 'Hashtag Channel');
// Check the "Try decrypting" checkbox // Check the "Try decrypting" checkbox
const checkbox = screen.getByRole('checkbox', { name: /Try decrypting/ }); const checkbox = screen.getByRole('checkbox', { name: /Try decrypting/ });
@@ -210,7 +188,7 @@ describe('NewMessageModal form reset', () => {
// Switch tab and come back // Switch tab and come back
await switchToTab(user, 'Contact'); await switchToTab(user, 'Contact');
await switchToTab(user, 'Hashtag'); await switchToTab(user, 'Hashtag Channel');
// The streaming message should be gone (tryHistorical was reset) // The streaming message should be gone (tryHistorical was reset)
expect(screen.queryByText(/Messages will stream in/)).toBeNull(); expect(screen.queryByText(/Messages will stream in/)).toBeNull();