mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-06-24 12:01:42 +02:00
Add option to disable scroll. Closes #299.
This commit is contained in:
+2
-2
@@ -507,9 +507,9 @@ PYTHONPATH=. uv run pytest tests/ -v
|
||||
|
||||
This is intentional. In the sidebar, unread direct messages for actual contact conversations are treated as mention-equivalent for badge styling. That means both the Contacts section header and contact unread badges themselves use the highlighted mention-style colors for unread DMs, including when those contacts appear in Favorites. Repeaters do not inherit this rule, and channel badges still use mention styling only for real `@[name]` mentions.
|
||||
|
||||
### RawPacketList always scrolls to bottom
|
||||
### RawPacketList autoscroll
|
||||
|
||||
`RawPacketList` unconditionally scrolls to the latest packet on every update. This is intentional — the packet feed is a live status display, not an interactive log meant for lingering or long-term analysis. Users watching it want to see the newest packet, not hold a scroll position.
|
||||
`RawPacketList` sticks to the latest packet on every update when its `autoScroll` prop is true (the default). `RawPacketFeedView` exposes an "Autoscroll" checkbox next to the type filters (default ticked, session-only — intentionally not persisted) so users can pause scrolling to correlate older packets. Toggling it back on jumps to the bottom immediately (`autoScroll` is an effect dependency).
|
||||
|
||||
## Editing Checklist
|
||||
|
||||
|
||||
@@ -478,6 +478,8 @@ export function RawPacketFeedView({
|
||||
const [analyzeModalOpen, setAnalyzeModalOpen] = useState(false);
|
||||
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
|
||||
const [enabledTypes, setEnabledTypes] = useState<Set<string>>(() => new Set(KNOWN_PAYLOAD_TYPES));
|
||||
// Autoscroll defaults on; intentionally not persisted across refreshes.
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
|
||||
const decoderOptions = useMemo(() => createDecoderOptions(channels), [channels]);
|
||||
|
||||
@@ -638,6 +640,15 @@ export function RawPacketFeedView({
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<label className="ml-auto flex items-center gap-1 text-xs text-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoScroll}
|
||||
onChange={(e) => setAutoScroll(e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
Autoscroll
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -671,6 +682,15 @@ export function RawPacketFeedView({
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<label className="ml-auto flex items-center gap-1 text-xs text-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoScroll}
|
||||
onChange={(e) => setAutoScroll(e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
Autoscroll
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -680,6 +700,7 @@ export function RawPacketFeedView({
|
||||
packets={filteredPackets}
|
||||
channels={channels}
|
||||
onPacketClick={setSelectedPacket}
|
||||
autoScroll={autoScroll}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ interface RawPacketListProps {
|
||||
packets: RawPacket[];
|
||||
channels?: Channel[];
|
||||
onPacketClick?: (packet: RawPacket) => void;
|
||||
/** When true (default), the feed sticks to the newest packet. */
|
||||
autoScroll?: boolean;
|
||||
}
|
||||
|
||||
function formatTime(timestamp: number): string {
|
||||
@@ -58,7 +60,12 @@ function getRouteTypeLabel(routeType: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
export function RawPacketList({ packets, channels, onPacketClick }: RawPacketListProps) {
|
||||
export function RawPacketList({
|
||||
packets,
|
||||
channels,
|
||||
onPacketClick,
|
||||
autoScroll = true,
|
||||
}: RawPacketListProps) {
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const decoderOptions = useMemo(() => createDecoderOptions(channels), [channels]);
|
||||
|
||||
@@ -76,11 +83,13 @@ export function RawPacketList({ packets, channels, onPacketClick }: RawPacketLis
|
||||
[decodedPackets]
|
||||
);
|
||||
|
||||
// Stick to the newest packet while autoscroll is on. Toggling it back on also
|
||||
// jumps to the bottom immediately (autoScroll is a dependency).
|
||||
useEffect(() => {
|
||||
if (listRef.current) {
|
||||
if (autoScroll && listRef.current) {
|
||||
listRef.current.scrollTop = listRef.current.scrollHeight;
|
||||
}
|
||||
}, [packets]);
|
||||
}, [packets, autoScroll]);
|
||||
|
||||
if (packets.length === 0) {
|
||||
return (
|
||||
|
||||
@@ -138,6 +138,16 @@ describe('RawPacketFeedView', () => {
|
||||
expect(screen.getByText('Traffic Timeline')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows an Autoscroll toggle that is ticked by default and can be unchecked', () => {
|
||||
renderView();
|
||||
|
||||
const autoscroll = screen.getByLabelText('Autoscroll') as HTMLInputElement;
|
||||
expect(autoscroll.checked).toBe(true);
|
||||
|
||||
fireEvent.click(autoscroll);
|
||||
expect((screen.getByLabelText('Autoscroll') as HTMLInputElement).checked).toBe(false);
|
||||
});
|
||||
|
||||
it('analyzes a pasted raw packet without adding it to the live feed', () => {
|
||||
renderView({ channels: [TEST_CHANNEL] });
|
||||
|
||||
|
||||
@@ -36,4 +36,34 @@ describe('RawPacketList', () => {
|
||||
|
||||
expect(onPacketClick).toHaveBeenCalledWith(packet);
|
||||
});
|
||||
|
||||
it('sticks to the bottom on new packets when autoScroll is on, and holds when off', () => {
|
||||
Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
|
||||
configurable: true,
|
||||
get: () => 500,
|
||||
});
|
||||
try {
|
||||
const { container, rerender } = render(
|
||||
<RawPacketList packets={[createPacket({ id: 1 })]} autoScroll />
|
||||
);
|
||||
const list = container.querySelector('.overflow-y-auto') as HTMLElement;
|
||||
|
||||
rerender(
|
||||
<RawPacketList packets={[createPacket({ id: 1 }), createPacket({ id: 2 })]} autoScroll />
|
||||
);
|
||||
expect(list.scrollTop).toBe(500);
|
||||
|
||||
// Pause autoscroll, simulate the user scrolling up, then receive a packet.
|
||||
list.scrollTop = 0;
|
||||
rerender(
|
||||
<RawPacketList
|
||||
packets={[createPacket({ id: 1 }), createPacket({ id: 2 }), createPacket({ id: 3 })]}
|
||||
autoScroll={false}
|
||||
/>
|
||||
);
|
||||
expect(list.scrollTop).toBe(0);
|
||||
} finally {
|
||||
delete (HTMLElement.prototype as { scrollHeight?: number }).scrollHeight;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user