Some misc frontend cleanup grossness

This commit is contained in:
Jack Kingsman
2026-03-11 20:49:37 -07:00
parent 38c7277c9d
commit bc7506b0d9
4 changed files with 95 additions and 10 deletions

View File

@@ -14,7 +14,8 @@ interface MapViewProps {
}
// Calculate marker color based on how recently the contact was heard
function getMarkerColor(lastSeen: number): string {
function getMarkerColor(lastSeen: number | null | undefined): string {
if (lastSeen == null) return '#9ca3af';
const now = Date.now() / 1000;
const age = now - lastSeen;
const hour = 3600;
@@ -94,16 +95,17 @@ function MapBoundsHandler({
}
export function MapView({ contacts, focusedKey }: MapViewProps) {
const sevenDaysAgo = Date.now() / 1000 - 7 * 24 * 60 * 60;
// Filter to contacts with GPS coordinates, heard within the last 7 days.
// Always include the focused contact so "view on map" links work for older nodes.
const mappableContacts = useMemo(() => {
const sevenDaysAgo = Date.now() / 1000 - 7 * 24 * 60 * 60;
return contacts.filter(
(c) =>
isValidLocation(c.lat, c.lon) &&
(c.public_key === focusedKey || (c.last_seen != null && c.last_seen > sevenDaysAgo))
);
}, [contacts, focusedKey]);
}, [contacts, focusedKey, sevenDaysAgo]);
// Find the focused contact by key
const focusedContact = useMemo(() => {
@@ -111,6 +113,10 @@ export function MapView({ contacts, focusedKey }: MapViewProps) {
return mappableContacts.find((c) => c.public_key === focusedKey) || null;
}, [focusedKey, mappableContacts]);
const includesFocusedOutsideWindow =
focusedContact != null &&
(focusedContact.last_seen == null || focusedContact.last_seen <= sevenDaysAgo);
// Track marker refs to open popup programmatically
const markerRefs = useRef<Record<string, LeafletCircleMarker | null>>({});
@@ -137,6 +143,7 @@ export function MapView({ contacts, focusedKey }: MapViewProps) {
<span>
Showing {mappableContacts.length} contact{mappableContacts.length !== 1 ? 's' : ''} heard
in the last 7 days
{includesFocusedOutsideWindow ? ' plus the focused contact' : ''}
</span>
<div className="flex items-center gap-3">
<span className="flex items-center gap-1">
@@ -175,8 +182,12 @@ export function MapView({ contacts, focusedKey }: MapViewProps) {
{mappableContacts.map((contact) => {
const isRepeater = contact.type === CONTACT_TYPE_REPEATER;
const color = getMarkerColor(contact.last_seen!);
const color = getMarkerColor(contact.last_seen);
const displayName = contact.name || contact.public_key.slice(0, 12);
const lastHeardLabel =
contact.last_seen != null
? formatTime(contact.last_seen)
: 'Never heard by this server';
return (
<CircleMarker
@@ -201,9 +212,7 @@ export function MapView({ contacts, focusedKey }: MapViewProps) {
)}
{displayName}
</div>
<div className="text-xs text-gray-500 mt-1">
Last heard: {formatTime(contact.last_seen!)}
</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">
{contact.lat!.toFixed(5)}, {contact.lon!.toFixed(5)}
</div>

View File

@@ -232,14 +232,12 @@ export function useConversationMessages(
if (latestReconcileRequestIdRef.current !== requestId) return;
const dataWithPendingAck = data.map((msg) => applyPendingAck(msg));
setHasOlderMessages(dataWithPendingAck.length >= MESSAGE_PAGE_SIZE);
const merged = messageCache.reconcile(messagesRef.current, dataWithPendingAck);
if (!merged) return;
setMessages(merged);
syncSeenContent(merged);
if (dataWithPendingAck.length >= MESSAGE_PAGE_SIZE) {
setHasOlderMessages(true);
}
})
.catch((err) => {
if (isAbortError(err)) return;

View File

@@ -0,0 +1,55 @@
import { forwardRef } from 'react';
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { MapView } from '../components/MapView';
import type { Contact } from '../types';
vi.mock('react-leaflet', () => ({
MapContainer: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
TileLayer: () => null,
CircleMarker: forwardRef<
HTMLDivElement,
{ children: React.ReactNode; pathOptions?: { fillColor?: string } }
>(({ children, pathOptions }, ref) => (
<div ref={ref} data-fill-color={pathOptions?.fillColor}>
{children}
</div>
)),
Popup: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
useMap: () => ({
setView: vi.fn(),
fitBounds: vi.fn(),
}),
}));
describe('MapView', () => {
it('renders a never-heard fallback for a focused contact without last_seen', () => {
const contact: Contact = {
public_key: 'aa'.repeat(32),
name: 'Mystery Node',
type: 1,
flags: 0,
last_path: null,
last_path_len: -1,
out_path_hash_mode: -1,
route_override_path: null,
route_override_len: null,
route_override_hash_mode: null,
last_advert: null,
lat: 40,
lon: -74,
last_seen: null,
on_radio: false,
last_contacted: null,
last_read_at: null,
first_seen: null,
};
render(<MapView contacts={[contact]} focusedKey={contact.public_key} />);
expect(
screen.getByText(/showing 1 contact heard in the last 7 days plus the focused contact/i)
).toBeInTheDocument();
expect(screen.getByText('Last heard: Never heard by this server')).toBeInTheDocument();
});
});

View File

@@ -264,6 +264,29 @@ describe('useConversationMessages background reconcile ordering', () => {
expect(result.current.messages[0].text).toBe('newer snapshot');
expect(result.current.messages[0].acked).toBe(2);
});
it('clears stale hasOlderMessages when cached conversations reconcile to a short latest page', async () => {
const conv = createConversation();
const cachedMessage = createMessage({ id: 42, text: 'cached snapshot' });
messageCache.set(conv.id, {
messages: [cachedMessage],
seenContent: new Set([
`PRIV-${cachedMessage.conversation_key}-${cachedMessage.text}-${cachedMessage.sender_timestamp}`,
]),
hasOlderMessages: true,
});
mockGetMessages.mockResolvedValueOnce([cachedMessage]);
const { result } = renderHook(() => useConversationMessages(conv));
expect(result.current.messages).toHaveLength(1);
expect(result.current.hasOlderMessages).toBe(true);
await waitFor(() => expect(mockGetMessages).toHaveBeenCalledTimes(1));
await waitFor(() => expect(result.current.hasOlderMessages).toBe(false));
});
});
describe('useConversationMessages forward pagination', () => {