mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Begone, prefix matching; use the whole key you have
This commit is contained in:
File diff suppressed because one or more lines are too long
1
frontend/dist/assets/index-Bt8bmX1W.js.map
vendored
Normal file
1
frontend/dist/assets/index-Bt8bmX1W.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/assets/index-eCIKhXih.js.map
vendored
1
frontend/dist/assets/index-eCIKhXih.js.map
vendored
File diff suppressed because one or more lines are too long
2
frontend/dist/index.html
vendored
2
frontend/dist/index.html
vendored
@@ -13,7 +13,7 @@
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<script type="module" crossorigin src="/assets/index-eCIKhXih.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-Bt8bmX1W.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CnRBRJ10.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
clearLocalStorageConversationState,
|
||||
} from './utils/conversationState';
|
||||
import { formatTime } from './utils/messageParser';
|
||||
import { pubkeysMatch, getContactDisplayName } from './utils/pubkey';
|
||||
import { getContactDisplayName } from './utils/pubkey';
|
||||
import { parseHashConversation, updateUrlHash, getMapFocusHash } from './utils/urlHash';
|
||||
import { isValidLocation, calculateDistance, formatDistance } from './utils/pathUtils';
|
||||
import {
|
||||
@@ -167,7 +167,7 @@ export function App() {
|
||||
return msg.conversation_key === activeConv.id;
|
||||
}
|
||||
if (msg.type === 'PRIV' && activeConv.type === 'contact') {
|
||||
return msg.conversation_key && pubkeysMatch(activeConv.id, msg.conversation_key);
|
||||
return msg.conversation_key === activeConv.id;
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
@@ -519,12 +519,7 @@ export function App() {
|
||||
// Compute optimistic new state
|
||||
const wasFavorited = isFavorite(favorites, type, id);
|
||||
const optimisticFavorites = wasFavorited
|
||||
? favorites.filter((f) => {
|
||||
if (f.type !== type) return true;
|
||||
// Use prefix matching for contacts, exact match for channels
|
||||
if (type === 'contact') return !pubkeysMatch(f.id, id);
|
||||
return f.id !== id;
|
||||
})
|
||||
? favorites.filter((f) => !(f.type === type && f.id === id))
|
||||
: [...favorites, { type, id }];
|
||||
|
||||
// Optimistic update
|
||||
|
||||
@@ -5,11 +5,10 @@ import 'leaflet/dist/leaflet.css';
|
||||
import type { Contact } from '../types';
|
||||
import { formatTime } from '../utils/messageParser';
|
||||
import { CONTACT_TYPE_REPEATER } from '../types';
|
||||
import { pubkeysMatch } from '../utils/pubkey';
|
||||
|
||||
interface MapViewProps {
|
||||
contacts: Contact[];
|
||||
/** Public key (or prefix) of contact to focus on and open popup */
|
||||
/** Public key of contact to focus on and open popup */
|
||||
focusedKey?: string | null;
|
||||
}
|
||||
|
||||
@@ -102,10 +101,10 @@ export function MapView({ contacts, focusedKey }: MapViewProps) {
|
||||
);
|
||||
}, [contacts]);
|
||||
|
||||
// Find the focused contact by key prefix
|
||||
// Find the focused contact by key
|
||||
const focusedContact = useMemo(() => {
|
||||
if (!focusedKey) return null;
|
||||
return mappableContacts.find((c) => pubkeysMatch(c.public_key, focusedKey)) || null;
|
||||
return mappableContacts.find((c) => c.public_key === focusedKey) || null;
|
||||
}, [focusedKey, mappableContacts]);
|
||||
|
||||
// Track marker refs to open popup programmatically
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useEffect, useLayoutEffect, useRef, useCallback, useState, type ReactNo
|
||||
import type { Contact, Message, MessagePath, RadioConfig } from '../types';
|
||||
import { CONTACT_TYPE_REPEATER } from '../types';
|
||||
import { formatTime, parseSenderFromText } from '../utils/messageParser';
|
||||
import { pubkeysMatch } from '../utils/pubkey';
|
||||
import { formatHopCounts, type SenderInfo } from '../utils/pathUtils';
|
||||
import { ContactAvatar } from './ContactAvatar';
|
||||
import { PathModal } from './PathModal';
|
||||
@@ -201,10 +200,10 @@ export function MessageList({
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Look up contact by public key or prefix
|
||||
// Look up contact by public key
|
||||
const getContact = (conversationKey: string | null): Contact | null => {
|
||||
if (!conversationKey) return null;
|
||||
return contacts.find((c) => pubkeysMatch(c.public_key, conversationKey)) || null;
|
||||
return contacts.find((c) => c.public_key === conversationKey) || null;
|
||||
};
|
||||
|
||||
// Look up contact by name (for channel messages where we parse sender from text)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import type { Contact, Channel, Conversation, Favorite } from '../types';
|
||||
import { getStateKey, type ConversationTimes } from '../utils/conversationState';
|
||||
import { getPubkeyPrefix, getContactDisplayName } from '../utils/pubkey';
|
||||
import { getContactDisplayName } from '../utils/pubkey';
|
||||
import { ContactAvatar } from './ContactAvatar';
|
||||
import { CONTACT_TYPE_REPEATER } from '../utils/contactAvatar';
|
||||
import { isFavorite } from '../utils/favorites';
|
||||
@@ -96,7 +96,7 @@ export function Sidebar({
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
// Deduplicate contacts by 12-char prefix, preferring ones with names
|
||||
// Deduplicate contacts by public key, preferring ones with names
|
||||
// Also filter out any contacts with empty public keys
|
||||
const uniqueContacts = contacts
|
||||
.filter((c) => c.public_key && c.public_key.length > 0)
|
||||
@@ -107,8 +107,7 @@ export function Sidebar({
|
||||
return (a.name || '').localeCompare(b.name || '');
|
||||
})
|
||||
.reduce<Contact[]>((acc, contact) => {
|
||||
const prefix = getPubkeyPrefix(contact.public_key);
|
||||
if (!acc.some((c) => getPubkeyPrefix(c.public_key) === prefix)) {
|
||||
if (!acc.some((c) => c.public_key === contact.public_key)) {
|
||||
acc.push(contact);
|
||||
}
|
||||
return acc;
|
||||
|
||||
@@ -349,12 +349,12 @@ describe('Integration: State Key Contract', () => {
|
||||
expect(stateKey).toBe(`channel-${channelKey}`);
|
||||
});
|
||||
|
||||
it('generates correct contact state key with prefix', () => {
|
||||
it('generates correct contact state key with full public key', () => {
|
||||
const publicKey = fixtures.advertisement_with_gps.expected_ws_event.data.public_key;
|
||||
|
||||
const stateKey = getStateKey('contact', publicKey);
|
||||
|
||||
// Contact state key uses 12-char prefix
|
||||
expect(stateKey).toBe(`contact-${publicKey.substring(0, 12)}`);
|
||||
// Contact state key uses full public key
|
||||
expect(stateKey).toBe(`contact-${publicKey}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,17 +104,10 @@ describe('getStateKey', () => {
|
||||
expect(key).toBe('channel-5');
|
||||
});
|
||||
|
||||
it('creates contact state key with 12-char prefix', () => {
|
||||
it('creates contact state key with full public key', () => {
|
||||
const fullKey = 'abcdef123456789012345678901234567890';
|
||||
const key = getStateKey('contact', fullKey);
|
||||
|
||||
expect(key).toBe('contact-abcdef123456');
|
||||
});
|
||||
|
||||
it('handles contact key shorter than 12 chars', () => {
|
||||
const shortKey = 'abc123';
|
||||
const key = getStateKey('contact', shortKey);
|
||||
|
||||
expect(key).toBe('contact-abc123');
|
||||
expect(key).toBe(`contact-${fullKey}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { Message, Conversation } from '../types';
|
||||
import { getPubkeyPrefix, pubkeysMatch } from '../utils/pubkey';
|
||||
|
||||
/**
|
||||
* Determine if a message should increment unread count.
|
||||
@@ -32,13 +31,9 @@ function shouldIncrementUnread(
|
||||
}
|
||||
|
||||
if (msg.type === 'PRIV' && msg.conversation_key) {
|
||||
// Use 12-char prefix for contact key
|
||||
const key = `contact-${getPubkeyPrefix(msg.conversation_key)}`;
|
||||
// Don't count if this contact is active (compare by prefix)
|
||||
if (
|
||||
activeConversation?.type === 'contact' &&
|
||||
pubkeysMatch(activeConversation.id, msg.conversation_key)
|
||||
) {
|
||||
const key = `contact-${msg.conversation_key}`;
|
||||
// Don't count if this contact is active
|
||||
if (activeConversation?.type === 'contact' && activeConversation.id === msg.conversation_key) {
|
||||
return null;
|
||||
}
|
||||
return { key };
|
||||
@@ -56,12 +51,7 @@ function getUnreadCount(
|
||||
id: string,
|
||||
unreadCounts: Record<string, number>
|
||||
): number {
|
||||
if (type === 'channel') {
|
||||
return unreadCounts[`channel-${id}`] || 0;
|
||||
}
|
||||
// For contacts, use prefix
|
||||
const prefix = `contact-${getPubkeyPrefix(id)}`;
|
||||
return unreadCounts[prefix] || 0;
|
||||
return unreadCounts[`${type}-${id}`] || 0;
|
||||
}
|
||||
|
||||
describe('shouldIncrementUnread', () => {
|
||||
@@ -137,17 +127,21 @@ describe('shouldIncrementUnread', () => {
|
||||
|
||||
const result = shouldIncrementUnread(msg, activeConversation);
|
||||
|
||||
expect(result).toEqual({ key: 'contact-abc123456789' });
|
||||
// State key uses full public key
|
||||
expect(result).toEqual({
|
||||
key: 'contact-abc123456789012345678901234567890123456789012345678901234567',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null for incoming direct message when viewing that contact', () => {
|
||||
const fullKey = 'abc123456789012345678901234567890123456789012345678901234567';
|
||||
const msg = createMessage({
|
||||
type: 'PRIV',
|
||||
conversation_key: 'abc123456789012345678901234567890123456789012345678901234567',
|
||||
conversation_key: fullKey,
|
||||
});
|
||||
const activeConversation: Conversation = {
|
||||
type: 'contact',
|
||||
id: 'abc123456789fullkey12345678901234567890123456789012345678',
|
||||
id: fullKey, // Same full key - exact match required
|
||||
name: 'Alice',
|
||||
};
|
||||
|
||||
@@ -193,23 +187,15 @@ describe('getUnreadCount', () => {
|
||||
expect(getUnreadCount('channel', 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB9', counts)).toBe(0);
|
||||
});
|
||||
|
||||
it('returns count for contact using 12-char prefix', () => {
|
||||
const counts = { 'contact-abc123456789': 5 };
|
||||
it('returns count for contact using full public key', () => {
|
||||
const fullKey = 'abc123456789fullpublickey123456789012345678901234';
|
||||
const counts = { [`contact-${fullKey}`]: 5 };
|
||||
|
||||
// Full public key lookup should match the prefix
|
||||
expect(
|
||||
getUnreadCount('contact', 'abc123456789fullpublickey123456789012345678901234', counts)
|
||||
).toBe(5);
|
||||
});
|
||||
|
||||
it('handles contact key shorter than 12 chars', () => {
|
||||
const counts = { 'contact-short': 2 };
|
||||
|
||||
expect(getUnreadCount('contact', 'short', counts)).toBe(2);
|
||||
expect(getUnreadCount('contact', fullKey, counts)).toBe(5);
|
||||
});
|
||||
|
||||
it('returns 0 for contact with no unread', () => {
|
||||
const counts = { 'contact-abc123456789': 5 };
|
||||
const counts = { 'contact-abc123456789fullpublickey123456789012345678901234': 5 };
|
||||
|
||||
expect(
|
||||
getUnreadCount('contact', 'xyz999999999fullkey12345678901234567890123456789', counts)
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
* across devices - see useUnreadCounts hook.
|
||||
*/
|
||||
|
||||
import { getPubkeyPrefix } from './pubkey';
|
||||
|
||||
const LAST_MESSAGE_KEY = 'remoteterm-lastMessageTime';
|
||||
const SORT_ORDER_KEY = 'remoteterm-sortOrder';
|
||||
|
||||
@@ -49,17 +47,10 @@ export function setLastMessageTime(key: string, timestamp: number): Conversation
|
||||
* This is NOT the same as Message.conversation_key (the database field).
|
||||
* This creates prefixed keys for state tracking:
|
||||
* - Channels: "channel-{channelKey}"
|
||||
* - Contacts: "contact-{12-char-pubkey-prefix}"
|
||||
*
|
||||
* The 12-char prefix for contacts ensures consistent matching regardless
|
||||
* of whether we have a full 64-char pubkey or just a prefix.
|
||||
* - Contacts: "contact-{publicKey}"
|
||||
*/
|
||||
export function getStateKey(type: 'channel' | 'contact', id: string): string {
|
||||
if (type === 'channel') {
|
||||
return `channel-${id}`;
|
||||
}
|
||||
// For contacts, use 12-char prefix for consistent matching
|
||||
return `contact-${getPubkeyPrefix(id)}`;
|
||||
return `${type}-${id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,27 +7,18 @@
|
||||
*/
|
||||
|
||||
import type { Favorite } from '../types';
|
||||
import { pubkeysMatch } from './pubkey';
|
||||
|
||||
const FAVORITES_KEY = 'remoteterm-favorites';
|
||||
|
||||
/**
|
||||
* Check if a conversation is favorited (from provided favorites array)
|
||||
*
|
||||
* For contacts, uses prefix matching to handle full pubkeys vs 12-char prefixes.
|
||||
*/
|
||||
export function isFavorite(
|
||||
favorites: Favorite[],
|
||||
type: 'channel' | 'contact',
|
||||
id: string
|
||||
): boolean {
|
||||
return favorites.some((f) => {
|
||||
if (f.type !== type) return false;
|
||||
// For contacts, use prefix matching (handles full keys vs prefixes)
|
||||
if (type === 'contact') return pubkeysMatch(f.id, id);
|
||||
// For channels, exact match
|
||||
return f.id === id;
|
||||
});
|
||||
return favorites.some((f) => f.type === type && f.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user