mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-05-11 07:26:53 +00:00
feat(auto-propagation) fix to not get stuck on a node we cant reach.
This commit is contained in:
@@ -106,8 +106,27 @@ class AutoPropagationManager:
|
||||
ctx = self.context
|
||||
router = ctx.message_router
|
||||
|
||||
previous_hex = (
|
||||
self.config.lxmf_preferred_propagation_node_destination_hash.get()
|
||||
)
|
||||
|
||||
# If a sync is in progress, only interrupt it when the current node
|
||||
# appears unreachable. This prevents getting stuck on a node we
|
||||
# cannot get a path to.
|
||||
if router.propagation_transfer_state != LXMRouter.PR_IDLE:
|
||||
return
|
||||
current_has_path = False
|
||||
if previous_hex:
|
||||
try:
|
||||
current_dest = bytes.fromhex(previous_hex)
|
||||
current_has_path = RNS.Transport.has_path(current_dest)
|
||||
except Exception:
|
||||
pass
|
||||
if current_has_path:
|
||||
# Sync is likely making progress – let it finish.
|
||||
return
|
||||
# Current node is unreachable – stop the stuck sync so we can
|
||||
# look for a working alternative.
|
||||
self.app.stop_propagation_node_sync(context=ctx)
|
||||
|
||||
announces = self.database.announces.get_announces(aspect="lxmf.propagation")
|
||||
|
||||
@@ -133,9 +152,6 @@ class AutoPropagationManager:
|
||||
if not sorted_candidates:
|
||||
return
|
||||
|
||||
previous_hex = (
|
||||
self.config.lxmf_preferred_propagation_node_destination_hash.get()
|
||||
)
|
||||
ordered: list[tuple[int, str]] = []
|
||||
seen_hex: set[str] = set()
|
||||
if previous_hex and previous_hex in best_by_hex:
|
||||
@@ -172,7 +188,18 @@ class AutoPropagationManager:
|
||||
)
|
||||
return
|
||||
|
||||
# None of the candidates worked. If the previously-selected node is
|
||||
# still unreachable, clear it rather than restoring a broken node.
|
||||
if previous_hex:
|
||||
self.app.set_active_propagation_node(previous_hex, context=self.context)
|
||||
try:
|
||||
previous_dest = bytes.fromhex(previous_hex)
|
||||
if RNS.Transport.has_path(previous_dest):
|
||||
self.app.set_active_propagation_node(
|
||||
previous_hex, context=self.context
|
||||
)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
self.app.remove_active_propagation_node(context=self.context)
|
||||
else:
|
||||
self.app.remove_active_propagation_node(context=self.context)
|
||||
|
||||
@@ -101,6 +101,18 @@
|
||||
>
|
||||
<MaterialDesignIcon icon-name="phone" class="w-5 h-5 sm:w-6 sm:h-6" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="sm:hidden rounded-full p-1.5 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 transition-colors"
|
||||
:title="isSyncingPropagationNode ? $t('app.syncing') : $t('app.sync_messages')"
|
||||
@click="syncPropagationNode"
|
||||
>
|
||||
<MaterialDesignIcon
|
||||
icon-name="refresh"
|
||||
class="w-5 h-5"
|
||||
:class="{ 'animate-spin': isSyncingPropagationNode }"
|
||||
/>
|
||||
</button>
|
||||
<button type="button" class="hidden sm:flex rounded-full" @click="syncPropagationNode">
|
||||
<span
|
||||
class="flex text-gray-800 dark:text-zinc-100 bg-white dark:bg-zinc-800/80 border border-gray-200 dark:border-zinc-700 hover:border-blue-400 dark:hover:border-blue-400/60 px-3 py-1.5 rounded-full shadow-xs transition"
|
||||
|
||||
@@ -228,11 +228,15 @@ export default {
|
||||
.toast-swipe-out-left {
|
||||
transform: translateX(-120%) !important;
|
||||
opacity: 0 !important;
|
||||
transition: transform 0.25s ease, opacity 0.25s ease !important;
|
||||
transition:
|
||||
transform 0.25s ease,
|
||||
opacity 0.25s ease !important;
|
||||
}
|
||||
.toast-swipe-out-right {
|
||||
transform: translateX(120%) !important;
|
||||
opacity: 0 !important;
|
||||
transition: transform 0.25s ease, opacity 0.25s ease !important;
|
||||
transition:
|
||||
transform 0.25s ease,
|
||||
opacity 0.25s ease !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,8 +15,7 @@ _VALID_HASH_C = "03" * 16
|
||||
_APP_DATA_ENABLED = b"\x94\x00\x00\x01\x00"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_propagation_logic():
|
||||
def _make_manager():
|
||||
app = MagicMock()
|
||||
context = MagicMock()
|
||||
config = MagicMock()
|
||||
@@ -30,6 +29,12 @@ async def test_auto_propagation_logic():
|
||||
context.message_router.propagation_transfer_state = LXMRouter.PR_IDLE
|
||||
|
||||
manager = AutoPropagationManager(app, context)
|
||||
return manager, app, context, config, database
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_propagation_logic():
|
||||
manager, app, context, config, database = _make_manager()
|
||||
|
||||
config.lxmf_preferred_propagation_node_auto_select.get.return_value = False
|
||||
with patch.object(manager, "check_and_update_propagation_node") as mock_check:
|
||||
@@ -115,3 +120,97 @@ async def test_auto_propagation_logic():
|
||||
await manager.check_and_update_propagation_node()
|
||||
|
||||
app.set_active_propagation_node.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_propagation_skips_when_sync_active_and_path_exists():
|
||||
"""Skip auto-propagation changes while sync is active and the path exists.
|
||||
|
||||
When a sync is active and the current node still has a path, the manager
|
||||
should leave it alone so the transfer can finish.
|
||||
"""
|
||||
manager, app, context, config, database = _make_manager()
|
||||
|
||||
config.lxmf_preferred_propagation_node_auto_select.get.return_value = True
|
||||
config.lxmf_preferred_propagation_node_destination_hash.get.return_value = (
|
||||
_VALID_HASH_A
|
||||
)
|
||||
context.message_router.propagation_transfer_state = LXMRouter.PR_RECEIVING
|
||||
|
||||
with patch.object(RNS.Transport, "has_path", return_value=True):
|
||||
await manager.check_and_update_propagation_node()
|
||||
|
||||
app.stop_propagation_node_sync.assert_not_called()
|
||||
app.set_active_propagation_node.assert_not_called()
|
||||
app.remove_active_propagation_node.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_propagation_finds_new_node_when_sync_stuck_no_path():
|
||||
"""Recover when sync is stuck and the current node has no path.
|
||||
|
||||
The manager should stop the stuck sync and look for a working node.
|
||||
"""
|
||||
manager, app, context, config, database = _make_manager()
|
||||
|
||||
config.lxmf_preferred_propagation_node_auto_select.get.return_value = True
|
||||
config.lxmf_preferred_propagation_node_destination_hash.get.return_value = (
|
||||
_VALID_HASH_A
|
||||
)
|
||||
context.message_router.propagation_transfer_state = LXMRouter.PR_PATH_REQUESTED
|
||||
|
||||
announce1 = {
|
||||
"destination_hash": _VALID_HASH_B,
|
||||
"app_data": _APP_DATA_ENABLED,
|
||||
}
|
||||
database.announces.get_announces.return_value = [announce1]
|
||||
|
||||
with (
|
||||
patch.object(RNS.Transport, "has_path") as mock_has_path,
|
||||
patch.object(RNS.Transport, "hops_to", return_value=1),
|
||||
patch.object(manager, "_wait_for_path", return_value=True),
|
||||
patch.object(manager, "_probe_propagation_sync", return_value=True),
|
||||
):
|
||||
# Current node A has no path, candidate B has a path.
|
||||
mock_has_path.side_effect = lambda dh: dh == bytes.fromhex(_VALID_HASH_B)
|
||||
|
||||
await manager.check_and_update_propagation_node()
|
||||
|
||||
app.stop_propagation_node_sync.assert_called_once_with(context=context)
|
||||
app.set_active_propagation_node.assert_called_once_with(
|
||||
_VALID_HASH_B,
|
||||
context=context,
|
||||
)
|
||||
config.lxmf_preferred_propagation_node_destination_hash.set.assert_called_with(
|
||||
_VALID_HASH_B,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_propagation_removes_broken_node_when_all_candidates_fail():
|
||||
"""Remove the active propagation node when no candidate works.
|
||||
|
||||
When no candidate works and the previous node is unreachable, the active
|
||||
propagation node should be removed instead of restoring a broken one.
|
||||
"""
|
||||
manager, app, context, config, database = _make_manager()
|
||||
|
||||
config.lxmf_preferred_propagation_node_auto_select.get.return_value = True
|
||||
config.lxmf_preferred_propagation_node_destination_hash.get.return_value = (
|
||||
_VALID_HASH_A
|
||||
)
|
||||
|
||||
announce1 = {
|
||||
"destination_hash": _VALID_HASH_B,
|
||||
"app_data": _APP_DATA_ENABLED,
|
||||
}
|
||||
database.announces.get_announces.return_value = [announce1]
|
||||
|
||||
with (
|
||||
patch.object(RNS.Transport, "has_path", return_value=False),
|
||||
patch.object(manager, "_wait_for_path", return_value=False),
|
||||
):
|
||||
await manager.check_and_update_propagation_node()
|
||||
|
||||
app.set_active_propagation_node.assert_not_called()
|
||||
app.remove_active_propagation_node.assert_called_once_with(context=context)
|
||||
|
||||
@@ -573,6 +573,35 @@ describe("Conditional Rendering", () => {
|
||||
const sidebarButton = wrapper.find("button.sm\\:hidden");
|
||||
expect(sidebarButton.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("App shows propagation sync refresh icon on mobile", async () => {
|
||||
const wrapper = mount(App, {
|
||||
global: {
|
||||
stubs: {
|
||||
RouterView: { template: "<div>Router View</div>" },
|
||||
RouterLink: createRouterLinkStub(),
|
||||
MaterialDesignIcon: { template: '<div data-icon-name="{{ iconName }}"></div>' },
|
||||
LanguageSelector: { template: "<div></div>" },
|
||||
NotificationBell: { template: "<div></div>" },
|
||||
SidebarLink: {
|
||||
template: '<div><slot name="icon"></slot><slot name="text"></slot></div>',
|
||||
props: ["to", "isCollapsed"],
|
||||
},
|
||||
},
|
||||
mocks: {
|
||||
$route: { name: "messages", meta: {}, query: {} },
|
||||
$router: { push: vi.fn() },
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mobileRefreshButtons = wrapper.findAll("button").filter((b) => {
|
||||
const cls = b.classes().join(" ");
|
||||
return cls.includes("sm:hidden") && b.attributes("title") === "app.sync_messages";
|
||||
});
|
||||
expect(mobileRefreshButtons.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Dark Mode Class Application", () => {
|
||||
|
||||
Reference in New Issue
Block a user