- {/* Raw path summary */}
-
- {paths.map((p, index) => {
- const hops = parsePathHops(p.path);
- const rawPath = hops.length > 0 ? hops.join('->') : 'direct';
- return (
-
- Path {index + 1}:{' '}
- {rawPath}
-
- );
- })}
-
+ {hasPaths && (
+
+ {/* Raw path summary */}
+
+ {paths.map((p, index) => {
+ const hops = parsePathHops(p.path);
+ const rawPath = hops.length > 0 ? hops.join('->') : 'direct';
+ return (
+
+ Path {index + 1}:{' '}
+ {rawPath}
+
+ );
+ })}
+
- {/* Straight-line distance (sender to receiver, same for all routes) */}
- {resolvedPaths.length > 0 &&
- isValidLocation(
- resolvedPaths[0].resolved.sender.lat,
- resolvedPaths[0].resolved.sender.lon
- ) &&
- isValidLocation(
- resolvedPaths[0].resolved.receiver.lat,
- resolvedPaths[0].resolved.receiver.lon
- ) && (
-
- Straight-line distance:
-
- {formatDistance(
- calculateDistance(
- resolvedPaths[0].resolved.sender.lat,
- resolvedPaths[0].resolved.sender.lon,
- resolvedPaths[0].resolved.receiver.lat,
- resolvedPaths[0].resolved.receiver.lon
- )!
- )}
-
-
- )}
-
- {resolvedPaths.map((pathData, index) => (
-
- {!hasSinglePath && (
-
- Path {index + 1}{' '}
-
- — received {formatTime(pathData.received_at)}
+ {/* Straight-line distance (sender to receiver, same for all routes) */}
+ {resolvedPaths.length > 0 &&
+ isValidLocation(
+ resolvedPaths[0].resolved.sender.lat,
+ resolvedPaths[0].resolved.sender.lon
+ ) &&
+ isValidLocation(
+ resolvedPaths[0].resolved.receiver.lat,
+ resolvedPaths[0].resolved.receiver.lon
+ ) && (
+
+ Straight-line distance:
+
+ {formatDistance(
+ calculateDistance(
+ resolvedPaths[0].resolved.sender.lat,
+ resolvedPaths[0].resolved.sender.lon,
+ resolvedPaths[0].resolved.receiver.lat,
+ resolvedPaths[0].resolved.receiver.lon
+ )!
+ )}
)}
-
-
- ))}
-
-
-
+ {resolvedPaths.map((pathData, index) => (
+
+ {!hasSinglePath && (
+
+ Path {index + 1}{' '}
+
+ — received {formatTime(pathData.received_at)}
+
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+ {hasResendActions && (
+
+ {isResendable && (
+
+ )}
+
+
+ )}
+
diff --git a/frontend/src/hooks/useConversationRouter.ts b/frontend/src/hooks/useConversationRouter.ts
index b8a4372..94d3c0e 100644
--- a/frontend/src/hooks/useConversationRouter.ts
+++ b/frontend/src/hooks/useConversationRouter.ts
@@ -131,7 +131,7 @@ export function useConversationRouter({
setActiveConversationState(publicConversation);
hasSetDefaultConversation.current = true;
}
- }, [channels, activeConversation, getPublicChannelConversation]);
+ }, [channels, activeConversation, getPublicChannelConversation, hasSetDefaultConversation]);
// Phase 2: Resolve contact hash (only if phase 1 didn't set a conversation)
useEffect(() => {
@@ -186,7 +186,14 @@ export function useConversationRouter({
hasSetDefaultConversation.current = true;
}
}
- }, [contacts, channels, activeConversation, contactsLoaded, getPublicChannelConversation]);
+ }, [
+ contacts,
+ channels,
+ activeConversation,
+ contactsLoaded,
+ getPublicChannelConversation,
+ hasSetDefaultConversation,
+ ]);
// Keep ref in sync and update URL hash
useEffect(() => {
@@ -221,7 +228,7 @@ export function useConversationRouter({
id: publicChannel.key,
name: publicChannel.name,
});
- }, [activeConversation, channels]);
+ }, [activeConversation, channels, hasSetDefaultConversation, pendingDeleteFallbackRef]);
// Handle conversation selection (closes sidebar on mobile)
const handleSelectConversation = useCallback(
diff --git a/tests/test_send_messages.py b/tests/test_send_messages.py
index 23877df..44ed3b1 100644
--- a/tests/test_send_messages.py
+++ b/tests/test_send_messages.py
@@ -283,7 +283,7 @@ class TestResendChannelMessage:
assert msg_id is not None
with patch("app.routers.messages.require_connected", return_value=mc):
- result = await resend_channel_message(msg_id)
+ result = await resend_channel_message(msg_id, new_timestamp=False)
assert result["status"] == "ok"
assert result["message_id"] == msg_id
@@ -316,11 +316,42 @@ class TestResendChannelMessage:
patch("app.routers.messages.require_connected", return_value=mc),
pytest.raises(HTTPException) as exc_info,
):
- await resend_channel_message(msg_id)
+ await resend_channel_message(msg_id, new_timestamp=False)
assert exc_info.value.status_code == 400
assert "expired" in exc_info.value.detail.lower()
+ @pytest.mark.asyncio
+ async def test_resend_new_timestamp_collision_returns_original_id(self, test_db):
+ """When new-timestamp resend collides (same second), return original ID gracefully."""
+ mc = _make_mc(name="MyNode")
+ chan_key = "dd" * 16
+ await ChannelRepository.upsert(key=chan_key, name="#collision")
+
+ now = int(time.time())
+ msg_id = await MessageRepository.create(
+ msg_type="CHAN",
+ text="MyNode: duplicate",
+ conversation_key=chan_key.upper(),
+ sender_timestamp=now,
+ received_at=now,
+ outgoing=True,
+ )
+ assert msg_id is not None
+
+ with (
+ patch("app.routers.messages.require_connected", return_value=mc),
+ patch("app.routers.messages.broadcast_event"),
+ patch("app.routers.messages.time") as mock_time,
+ ):
+ # Force the same second so MessageRepository.create returns None (duplicate)
+ mock_time.time.return_value = float(now)
+ result = await resend_channel_message(msg_id, new_timestamp=True)
+
+ # Should succeed gracefully, returning the original message ID
+ assert result["status"] == "ok"
+ assert result["message_id"] == msg_id
+
@pytest.mark.asyncio
async def test_resend_non_outgoing_returns_400(self, test_db):
"""Resend of incoming message fails."""
@@ -343,7 +374,7 @@ class TestResendChannelMessage:
patch("app.routers.messages.require_connected", return_value=mc),
pytest.raises(HTTPException) as exc_info,
):
- await resend_channel_message(msg_id)
+ await resend_channel_message(msg_id, new_timestamp=False)
assert exc_info.value.status_code == 400
assert "outgoing" in exc_info.value.detail.lower()
@@ -369,7 +400,7 @@ class TestResendChannelMessage:
patch("app.routers.messages.require_connected", return_value=mc),
pytest.raises(HTTPException) as exc_info,
):
- await resend_channel_message(msg_id)
+ await resend_channel_message(msg_id, new_timestamp=False)
assert exc_info.value.status_code == 400
assert "channel" in exc_info.value.detail.lower()
@@ -383,7 +414,7 @@ class TestResendChannelMessage:
patch("app.routers.messages.require_connected", return_value=mc),
pytest.raises(HTTPException) as exc_info,
):
- await resend_channel_message(999999)
+ await resend_channel_message(999999, new_timestamp=False)
assert exc_info.value.status_code == 404
@@ -406,7 +437,126 @@ class TestResendChannelMessage:
assert msg_id is not None
with patch("app.routers.messages.require_connected", return_value=mc):
- await resend_channel_message(msg_id)
+ await resend_channel_message(msg_id, new_timestamp=False)
call_kwargs = mc.commands.send_chan_msg.await_args.kwargs
assert call_kwargs["msg"] == "hello world"
+
+ @pytest.mark.asyncio
+ async def test_resend_new_timestamp_skips_window(self, test_db):
+ """new_timestamp=True succeeds even when the 30s window has expired."""
+ mc = _make_mc(name="MyNode")
+ chan_key = "dd" * 16
+ await ChannelRepository.upsert(key=chan_key, name="#old")
+
+ old_ts = int(time.time()) - 60 # 60 seconds ago — outside byte-perfect window
+ msg_id = await MessageRepository.create(
+ msg_type="CHAN",
+ text="MyNode: old message",
+ conversation_key=chan_key.upper(),
+ sender_timestamp=old_ts,
+ received_at=old_ts,
+ outgoing=True,
+ )
+ assert msg_id is not None
+
+ with (
+ patch("app.routers.messages.require_connected", return_value=mc),
+ patch("app.routers.messages.broadcast_event"),
+ ):
+ result = await resend_channel_message(msg_id, new_timestamp=True)
+
+ assert result["status"] == "ok"
+ # Should return a NEW message id, not the original
+ assert result["message_id"] != msg_id
+
+ @pytest.mark.asyncio
+ async def test_resend_new_timestamp_creates_new_message(self, test_db):
+ """new_timestamp=True creates a new DB row with a different sender_timestamp."""
+ mc = _make_mc(name="MyNode")
+ chan_key = "dd" * 16
+ await ChannelRepository.upsert(key=chan_key, name="#new")
+
+ old_ts = int(time.time()) - 10
+ msg_id = await MessageRepository.create(
+ msg_type="CHAN",
+ text="MyNode: test",
+ conversation_key=chan_key.upper(),
+ sender_timestamp=old_ts,
+ received_at=old_ts,
+ outgoing=True,
+ )
+ assert msg_id is not None
+
+ with (
+ patch("app.routers.messages.require_connected", return_value=mc),
+ patch("app.routers.messages.broadcast_event"),
+ ):
+ result = await resend_channel_message(msg_id, new_timestamp=True)
+
+ new_msg_id = result["message_id"]
+ new_msg = await MessageRepository.get_by_id(new_msg_id)
+ original_msg = await MessageRepository.get_by_id(msg_id)
+
+ assert new_msg is not None
+ assert original_msg is not None
+ assert new_msg.sender_timestamp != original_msg.sender_timestamp
+ assert new_msg.text == original_msg.text
+ assert new_msg.outgoing is True
+
+ @pytest.mark.asyncio
+ async def test_resend_new_timestamp_broadcasts_message(self, test_db):
+ """new_timestamp=True broadcasts the new message via WebSocket."""
+ mc = _make_mc(name="MyNode")
+ chan_key = "dd" * 16
+ await ChannelRepository.upsert(key=chan_key, name="#broadcast")
+
+ old_ts = int(time.time()) - 5
+ msg_id = await MessageRepository.create(
+ msg_type="CHAN",
+ text="MyNode: broadcast test",
+ conversation_key=chan_key.upper(),
+ sender_timestamp=old_ts,
+ received_at=old_ts,
+ outgoing=True,
+ )
+ assert msg_id is not None
+
+ with (
+ patch("app.routers.messages.require_connected", return_value=mc),
+ patch("app.routers.messages.broadcast_event") as mock_broadcast,
+ ):
+ result = await resend_channel_message(msg_id, new_timestamp=True)
+
+ mock_broadcast.assert_called_once()
+ event_type, event_data = mock_broadcast.call_args.args
+ assert event_type == "message"
+ assert event_data["id"] == result["message_id"]
+ assert event_data["outgoing"] is True
+
+ @pytest.mark.asyncio
+ async def test_resend_byte_perfect_still_enforces_window(self, test_db):
+ """Default (byte-perfect) resend still enforces the 30s window."""
+ mc = _make_mc(name="MyNode")
+ chan_key = "dd" * 16
+ await ChannelRepository.upsert(key=chan_key, name="#window")
+
+ old_ts = int(time.time()) - 60
+ msg_id = await MessageRepository.create(
+ msg_type="CHAN",
+ text="MyNode: expired",
+ conversation_key=chan_key.upper(),
+ sender_timestamp=old_ts,
+ received_at=old_ts,
+ outgoing=True,
+ )
+ assert msg_id is not None
+
+ with (
+ patch("app.routers.messages.require_connected", return_value=mc),
+ pytest.raises(HTTPException) as exc_info,
+ ):
+ await resend_channel_message(msg_id, new_timestamp=False)
+
+ assert exc_info.value.status_code == 400
+ assert "expired" in exc_info.value.detail.lower()
From 40dde4647a3b7fe707292a6a83a38822d447d625 Mon Sep 17 00:00:00 2001
From: Jack Kingsman
Date: Sat, 21 Feb 2026 17:08:26 -0800
Subject: [PATCH 14/17] Correct button alignment
---
frontend/src/components/PathModal.tsx | 30 +++++++++++++--------------
1 file changed, 14 insertions(+), 16 deletions(-)
diff --git a/frontend/src/components/PathModal.tsx b/frontend/src/components/PathModal.tsx
index 1721ba6..2cb5459 100644
--- a/frontend/src/components/PathModal.tsx
+++ b/frontend/src/components/PathModal.tsx
@@ -1,12 +1,5 @@
import type { Contact, RadioConfig, MessagePath } from '../types';
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogDescription,
- DialogFooter,
-} from './ui/dialog';
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
import { Button } from './ui/button';
import {
resolvePath,
@@ -143,24 +136,29 @@ export function PathModal({
)}
-