Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions src/renderer/components/ChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ import { reticulumHashForNodeId, useReticulumPeerStore } from '../stores/reticul
import { useTimeFormatStore } from '../stores/timeFormatStore';
import { ChatComposer, type ChatComposerSendOpts } from './ChatComposer';
import { ChatPayloadText } from './ChatPayloadText';
import { ChatRfHopLabel } from './ChatRfHopLabel';
import { HelpTooltip } from './HelpTooltip';
import { MessageStatusBadge } from './MessageStatusBadge';
import { ChatDmRncpControl } from './remote/ChatDmRncpControl';
Expand Down Expand Up @@ -2717,12 +2718,7 @@ function ChatPanel({
<div className="mt-0.5 flex items-center justify-end gap-2">
{msg.rxHops != null &&
(msg.receivedVia === 'rf' || msg.receivedVia === 'both') && (
<span
className="text-[10px] text-gray-500"
title={t('nodeDetailModal.hopsFromRoutingTitle')}
>
{t('nodeDetailModal.hopLabel', { count: msg.rxHops })}
</span>
<ChatRfHopLabel rxHops={msg.rxHops} msg={msg} />
)}
{msg.viaStoreForward && <StoreForwardBadge />}
{msg.receivedVia && (
Expand Down
52 changes: 52 additions & 0 deletions src/renderer/components/ChatRfHopLabel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// @vitest-environment jsdom
import { cleanup, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it } from 'vitest';

import {
markMeshcoreHopCorrected,
resetMeshcoreHopCorrectedMarksForTests,
} from '../lib/meshcoreLateRfHopEnrichment';
import { ChatRfHopLabel, chatRfHopLabelPresentation } from './ChatRfHopLabel';

describe('chatRfHopLabelPresentation', () => {
it('uses amber accent only when corrected and motion is allowed', () => {
expect(chatRfHopLabelPresentation(false, false).className).toContain('text-gray-500');
expect(chatRfHopLabelPresentation(true, false).className).toContain('text-amber-400');
expect(chatRfHopLabelPresentation(true, true).className).toContain('text-gray-500');
expect(chatRfHopLabelPresentation(true, true).refined).toBe(true);
expect(chatRfHopLabelPresentation(false, false).refined).toBe(false);
});
});

describe('ChatRfHopLabel', () => {
afterEach(() => {
cleanup();
resetMeshcoreHopCorrectedMarksForTests();
});

it('renders hop count with default title when not corrected', () => {
render(
<ChatRfHopLabel
rxHops={3}
msg={{ storeId: 'ch:0:1:x', sender_id: 2, timestamp: Date.now(), channel: 0 }}
/>,
);
expect(screen.getByText('3 hops')).toBeInTheDocument();
expect(screen.getByText('3 hops')).toHaveAttribute(
'title',
expect.stringMatching(/hop|routing/i),
);
});

it('uses refined title when a correction mark is active', () => {
markMeshcoreHopCorrected('ch:0:2:x');
render(
<ChatRfHopLabel
rxHops={4}
msg={{ storeId: 'ch:0:2:x', sender_id: 2, timestamp: Date.now(), channel: 0 }}
/>,
);
expect(screen.getByText('4 hops')).toHaveAttribute('title', 'Updated from RF path');
expect(screen.getByText('4 hops').className).toContain('text-amber-400');
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
65 changes: 65 additions & 0 deletions src/renderer/components/ChatRfHopLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useSyncExternalStore } from 'react';
import { useTranslation } from 'react-i18next';

import { useReduceMotion } from '@/renderer/lib/icons/iconMotionContext';
import {
isMeshcoreHopCorrected,
meshcoreChatHopUiKey,
subscribeMeshcoreHopCorrected,
} from '@/renderer/lib/meshcoreLateRfHopEnrichment';

export interface ChatRfHopLabelProps {
rxHops: number;
msg: {
storeId?: string;
id?: number;
sender_id: number;
timestamp: number;
channel: number;
};
}

/** Class/title for the hop pill when a late RF correction mark is active. */
export function chatRfHopLabelPresentation(
corrected: boolean,
reduceMotion: boolean,
): { className: string; refined: boolean } {
if (!corrected) {
return {
className: 'text-[10px] text-gray-500 transition-colors duration-500',
refined: false,
};
}
if (reduceMotion) {
return {
className: 'text-[10px] text-gray-500 transition-colors duration-500',
refined: true,
};
}
return {
className: 'text-[10px] text-amber-400/80 transition-colors duration-500',
refined: true,
};
}

/** Incoming RF hop count; briefly accents when late event 136 corrected a stored value. */
export function ChatRfHopLabel({ rxHops, msg }: ChatRfHopLabelProps) {
const { t } = useTranslation();
const reduceMotion = useReduceMotion();
const uiKey = meshcoreChatHopUiKey(msg);
const corrected = useSyncExternalStore(
subscribeMeshcoreHopCorrected,
() => isMeshcoreHopCorrected(uiKey),
() => false,
);
const { className, refined } = chatRfHopLabelPresentation(corrected, reduceMotion);
const title = refined
? t('chatPanel.hopCountRefinedFromRf')
: t('nodeDetailModal.hopsFromRoutingTitle');

return (
<span className={className} title={title} aria-label={title}>
{t('nodeDetailModal.hopLabel', { count: rxHops })}
</span>
);
}
16 changes: 16 additions & 0 deletions src/renderer/lib/meshcore/meshcoreRfRxRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
meshtasticSenderIdForRawLogFallback,
type PacketClass,
} from '../foreignLoraDetection';
import { applyMeshcoreLateRfHopEnrichment } from '../meshcoreLateRfHopEnrichment';
import {
meshcoreRawPacketLogFromBytesFallback,
meshcoreRawPacketResolveFromParsed,
Expand Down Expand Up @@ -642,6 +643,21 @@ export function handleMeshcoreRfRx(payload: MeshcoreRfRxPayload, deps: MeshcoreR
const rxEntry = buildMeshcoreRfRawPacketEntry(ctx, effectiveFromNodeId, now, snr, rssi, rawU8);
pushMeshcoreRfRawPacketLog(deps, rxEntry);

if (
ctx.parseOk &&
(ctx.payloadTypeString === 'TXT_MSG' || ctx.payloadTypeString === 'GRP_TXT')
) {
applyMeshcoreLateRfHopEnrichment(deps.meshcoreIdentityIdRef.current, {
payloadTypeString: ctx.payloadTypeString,
hopCount: ctx.hopCount,
fromNodeId: effectiveFromNodeId,
messageFingerprintHex: ctx.messageFingerprintHex,
parseOk: true,
now,
myNodeNum: deps.myNodeNumRef.current,
});
}

mqttFields = buildMeshcoreRfMqttPacketLogFields(ctx, rawU8);
recordMeshcoreRfNoisePorts(ctx, effectiveFromNodeId);

Expand Down
Loading