mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-11 03:03:03 +02:00
Adjust phrasing on new-chat modal, and remove the unusable existing-contact scren. Closes #105.
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user