mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-01 19:12:57 +02:00
Add link to node from map display. Closes #189.
This commit is contained in:
@@ -588,6 +588,7 @@ export function App() {
|
||||
onDeleteChannel: handleDeleteChannel,
|
||||
onSetChannelFloodScopeOverride: handleSetChannelFloodScopeOverride,
|
||||
onSetChannelPathHashModeOverride: handleSetChannelPathHashModeOverride,
|
||||
onSelectConversation: handleSelectConversationWithTargetReset,
|
||||
onOpenContactInfo: handleOpenContactInfo,
|
||||
onOpenChannelInfo: handleOpenChannelInfo,
|
||||
onSenderClick: handleSenderClick,
|
||||
|
||||
@@ -20,7 +20,11 @@ import type {
|
||||
} from '../types';
|
||||
import type { RawPacketStatsSessionState } from '../utils/rawPacketStats';
|
||||
import { CONTACT_TYPE_REPEATER, CONTACT_TYPE_ROOM } from '../types';
|
||||
import { isPrefixOnlyContact, isUnknownFullKeyContact } from '../utils/pubkey';
|
||||
import {
|
||||
getContactDisplayName,
|
||||
isPrefixOnlyContact,
|
||||
isUnknownFullKeyContact,
|
||||
} from '../utils/pubkey';
|
||||
|
||||
const RepeaterDashboard = lazy(() =>
|
||||
import('./RepeaterDashboard').then((m) => ({ default: m.RepeaterDashboard }))
|
||||
@@ -65,6 +69,7 @@ interface ConversationPaneProps {
|
||||
channelKey: string,
|
||||
pathHashModeOverride: number | null
|
||||
) => Promise<void>;
|
||||
onSelectConversation: (conversation: Conversation) => void;
|
||||
onOpenContactInfo: (publicKey: string, fromChannel?: boolean) => void;
|
||||
onOpenChannelInfo: (channelKey: string) => void;
|
||||
onSenderClick: (sender: string) => void;
|
||||
@@ -137,6 +142,7 @@ export function ConversationPane({
|
||||
onDeleteChannel,
|
||||
onSetChannelFloodScopeOverride,
|
||||
onSetChannelPathHashModeOverride,
|
||||
onSelectConversation,
|
||||
onOpenContactInfo,
|
||||
onOpenChannelInfo,
|
||||
onSenderClick,
|
||||
@@ -197,6 +203,17 @@ export function ConversationPane({
|
||||
focusedKey={activeConversation.mapFocusKey}
|
||||
rawPackets={rawPackets}
|
||||
config={config}
|
||||
onSelectContact={(contact) =>
|
||||
onSelectConversation({
|
||||
type: 'contact',
|
||||
id: contact.public_key,
|
||||
name: getContactDisplayName(
|
||||
contact.name,
|
||||
contact.public_key,
|
||||
contact.last_advert
|
||||
),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,9 @@ interface MapViewProps {
|
||||
focusedKey?: string | null;
|
||||
rawPackets?: RawPacket[];
|
||||
config?: RadioConfig | null;
|
||||
/** When provided, the contact name in each popup becomes a clickable link
|
||||
* that opens the conversation for that contact (DM, repeater, or room). */
|
||||
onSelectContact?: (contact: Contact) => void;
|
||||
}
|
||||
|
||||
// --- Tile layer presets ---
|
||||
@@ -379,7 +382,13 @@ function ParticleOverlay({ particles }: { particles: MapParticle[] }) {
|
||||
|
||||
// --- Main component ---
|
||||
|
||||
export function MapView({ contacts, focusedKey, rawPackets, config }: MapViewProps) {
|
||||
export function MapView({
|
||||
contacts,
|
||||
focusedKey,
|
||||
rawPackets,
|
||||
config,
|
||||
onSelectContact,
|
||||
}: MapViewProps) {
|
||||
const [sevenDaysAgo] = useState(() => Date.now() / 1000 - 7 * 24 * 60 * 60);
|
||||
const [darkMap, setDarkMap] = useState(getSavedDarkMap);
|
||||
const tile = darkMap ? TILE_DARK : TILE_LIGHT;
|
||||
@@ -839,7 +848,21 @@ export function MapView({ contacts, focusedKey, rawPackets, config }: MapViewPro
|
||||
🛜
|
||||
</span>
|
||||
)}
|
||||
{displayName}
|
||||
{onSelectContact ? (
|
||||
<button
|
||||
type="button"
|
||||
className="p-0 bg-transparent border-0 font-inherit text-primary underline hover:text-primary/80 cursor-pointer"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onSelectContact(contact);
|
||||
}}
|
||||
title={`Open conversation with ${displayName}`}
|
||||
>
|
||||
{displayName}
|
||||
</button>
|
||||
) : (
|
||||
displayName
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">Last heard: {lastHeardLabel}</div>
|
||||
<div className="text-xs text-gray-400 mt-1 font-mono">
|
||||
|
||||
@@ -145,6 +145,7 @@ function createProps(overrides: Partial<React.ComponentProps<typeof Conversation
|
||||
onDeleteContact: vi.fn(async () => {}),
|
||||
onDeleteChannel: vi.fn(async () => {}),
|
||||
onSetChannelFloodScopeOverride: vi.fn(async () => {}),
|
||||
onSelectConversation: vi.fn(),
|
||||
onOpenContactInfo: vi.fn(),
|
||||
onOpenChannelInfo: vi.fn(),
|
||||
onSenderClick: vi.fn(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { forwardRef } from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { MapView } from '../components/MapView';
|
||||
import type { Contact } from '../types';
|
||||
@@ -54,6 +54,68 @@ describe('MapView', () => {
|
||||
expect(screen.getByText('Last heard: Never heard by this server')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('invokes onSelectContact when the popup name is clicked', () => {
|
||||
const contact: Contact = {
|
||||
public_key: 'cc'.repeat(32),
|
||||
name: 'Clickable',
|
||||
type: 1,
|
||||
flags: 0,
|
||||
direct_path: null,
|
||||
direct_path_len: -1,
|
||||
direct_path_hash_mode: -1,
|
||||
route_override_path: null,
|
||||
route_override_len: null,
|
||||
route_override_hash_mode: null,
|
||||
last_advert: null,
|
||||
lat: 42,
|
||||
lon: -72,
|
||||
last_seen: Math.floor(Date.now() / 1000),
|
||||
on_radio: false,
|
||||
favorite: false,
|
||||
last_contacted: null,
|
||||
last_read_at: null,
|
||||
first_seen: null,
|
||||
};
|
||||
const onSelectContact = vi.fn();
|
||||
|
||||
render(<MapView contacts={[contact]} onSelectContact={onSelectContact} />);
|
||||
|
||||
const link = screen.getByRole('button', { name: 'Clickable' });
|
||||
expect(link).toHaveAttribute('title', 'Open conversation with Clickable');
|
||||
fireEvent.click(link);
|
||||
|
||||
expect(onSelectContact).toHaveBeenCalledWith(contact);
|
||||
});
|
||||
|
||||
it('renders the popup name as plain text when no onSelectContact is provided', () => {
|
||||
const contact: Contact = {
|
||||
public_key: 'dd'.repeat(32),
|
||||
name: 'Static',
|
||||
type: 1,
|
||||
flags: 0,
|
||||
direct_path: null,
|
||||
direct_path_len: -1,
|
||||
direct_path_hash_mode: -1,
|
||||
route_override_path: null,
|
||||
route_override_len: null,
|
||||
route_override_hash_mode: null,
|
||||
last_advert: null,
|
||||
lat: 42,
|
||||
lon: -72,
|
||||
last_seen: Math.floor(Date.now() / 1000),
|
||||
on_radio: false,
|
||||
favorite: false,
|
||||
last_contacted: null,
|
||||
last_read_at: null,
|
||||
first_seen: null,
|
||||
};
|
||||
|
||||
render(<MapView contacts={[contact]} />);
|
||||
|
||||
expect(screen.queryByRole('button', { name: /open conversation with static/i })).toBeNull();
|
||||
expect(screen.getByText('Static')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps the 7-day cutoff stable for the lifetime of the mounted map', () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user