diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py index af44548f..9fa317f8 100644 --- a/meshchatx/meshchat.py +++ b/meshchatx/meshchat.py @@ -5157,7 +5157,8 @@ class ReticulumMeshChat: { "message": ( "RNode serial and Bluetooth are not available on this device. " - "On Android, the app must include usbserial4a (see MeshChatX issue #6)." + "On Android, the app must include usbserial4a and jnius " + "(see MeshChatX issue #6)." ), }, status=422, diff --git a/meshchatx/src/backend/rrc/manager.py b/meshchatx/src/backend/rrc/manager.py index 26cee162..de5aa8cc 100644 --- a/meshchatx/src/backend/rrc/manager.py +++ b/meshchatx/src/backend/rrc/manager.py @@ -482,9 +482,22 @@ class RRCHub: def set_auto_list(self, enabled, save=True): with self._lock: self.auto_list = bool(enabled) + should_list = self.auto_list and self.welcomed if save: self.manager.save() self.manager._notify_change(self) + if should_list: + self._request_room_list() + + def _request_room_list(self): + try: + with self._lock: + self._silent_list_pending += 1 + self.send_command("/list", room=None, record_local=False) + except Exception: + with self._lock: + if self._silent_list_pending > 0: + self._silent_list_pending -= 1 def set_auto_who(self, enabled, save=True): with self._lock: @@ -950,14 +963,7 @@ class RRCHub: self._reconnect_attempts = 0 self.manager._on_welcome(self) if self.auto_list: - try: - with self._lock: - self._silent_list_pending += 1 - self.send_command("/list", room=None, record_local=False) - except Exception: - with self._lock: - if self._silent_list_pending > 0: - self._silent_list_pending -= 1 + self._request_room_list() def _apply_limits(self, limits): if proto.L_MAX_NICK_BYTES in limits: diff --git a/meshchatx/src/frontend/components/relay/RelayChatPage.vue b/meshchatx/src/frontend/components/relay/RelayChatPage.vue index a84cedc3..6e9694b7 100644 --- a/meshchatx/src/frontend/components/relay/RelayChatPage.vue +++ b/meshchatx/src/frontend/components/relay/RelayChatPage.vue @@ -236,13 +236,46 @@
  • {{ $t("relay_chat.no_rooms") }}
  • +
    +
    + {{ $t("relay_chat.available_rooms") }} +
    + +
    +
    !known.has(name)) + .map(([name, topic]) => ({ name, topic })) + .sort((a, b) => a.name.localeCompare(b.name)); + }, onCollapsedHubClick(hub) { const rooms = this.orderedRoomsFor(hub); this.selectedHubHash = hub.hub_hash; @@ -2032,13 +2075,23 @@ export default { ToastUtils.warning(this.$t("relay_chat.room_required")); return; } + const joined = await this.joinRoomByName(hub, room); + if (joined) { + this.joinRoomName = ""; + } + }, + async joinAvailableRoom(hub, room) { + await this.joinRoomByName(hub, room); + }, + async joinRoomByName(hub, room) { try { await window.api.post(`/api/v1/rrc/hubs/${hub.hub_hash}/rooms`, { room }); - this.joinRoomName = ""; ToastUtils.success(this.$t("relay_chat.joined_room")); await this.fetchHubs(); + return true; } catch (e) { ToastUtils.error(e.response?.data?.message || this.$t("relay_chat.action_failed")); + return false; } }, async leaveRoom() { diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json index 9bae63b7..e8bdb727 100644 --- a/meshchatx/src/frontend/locales/de.json +++ b/meshchatx/src/frontend/locales/de.json @@ -2904,6 +2904,8 @@ "dest_name": "Ziel-Aspekt (optional)", "no_hubs": "Noch keine Hubs konfiguriert. Fügen Sie einen hinzu, um zu beginnen.", "no_rooms": "Keine Räume beigetreten. Treten Sie einem Raum bei, um zu chatten.", + "available_rooms": "Verfügbare Räume", + "join": "Beitreten", "no_room_selected": "Wählen Sie einen Raum, um Nachrichten anzuzeigen.", "load_previous": "Frühere Nachrichten laden", "rooms": "Räume", diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json index 8cd50789..55eb4a92 100644 --- a/meshchatx/src/frontend/locales/en.json +++ b/meshchatx/src/frontend/locales/en.json @@ -2263,6 +2263,8 @@ "dest_name": "Destination Aspect (optional)", "no_hubs": "No hubs configured yet. Add one to get started.", "no_rooms": "No rooms joined. Join a room to start chatting.", + "available_rooms": "Available Rooms", + "join": "Join", "no_room_selected": "Select a room to view messages.", "load_previous": "Load earlier messages", "rooms": "Rooms", diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json index 8d794557..093e0cfd 100644 --- a/meshchatx/src/frontend/locales/es.json +++ b/meshchatx/src/frontend/locales/es.json @@ -2904,6 +2904,8 @@ "dest_name": "Aspecto de destino (opcional)", "no_hubs": "Aun no hay hubs configurados. Anade uno para empezar.", "no_rooms": "No te has unido a ninguna sala. Unete a una para chatear.", + "available_rooms": "Salas disponibles", + "join": "Unirse", "no_room_selected": "Selecciona una sala para ver los mensajes.", "load_previous": "Cargar mensajes anteriores", "rooms": "Salas", diff --git a/meshchatx/src/frontend/locales/fi.json b/meshchatx/src/frontend/locales/fi.json index 89581490..2ceafb79 100644 --- a/meshchatx/src/frontend/locales/fi.json +++ b/meshchatx/src/frontend/locales/fi.json @@ -2263,6 +2263,8 @@ "dest_name": "Destination Aspect (optional)", "no_hubs": "No hubs configured yet. Add one to get started.", "no_rooms": "No rooms joined. Join a room to start chatting.", + "available_rooms": "Available Rooms", + "join": "Join", "no_room_selected": "Select a room to view messages.", "load_previous": "Load earlier messages", "rooms": "Rooms", diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json index 43c6d35b..b62fcaa9 100644 --- a/meshchatx/src/frontend/locales/fr.json +++ b/meshchatx/src/frontend/locales/fr.json @@ -2904,6 +2904,8 @@ "dest_name": "Aspect de destination (optionnel)", "no_hubs": "Aucun hub configure. Ajoutez-en un pour commencer.", "no_rooms": "Aucun salon rejoint. Rejoignez un salon pour discuter.", + "available_rooms": "Salons disponibles", + "join": "Rejoindre", "no_room_selected": "Selectionnez un salon pour voir les messages.", "load_previous": "Charger les messages precedents", "rooms": "Salons", diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json index ef2c24e9..282656ff 100644 --- a/meshchatx/src/frontend/locales/it.json +++ b/meshchatx/src/frontend/locales/it.json @@ -2904,6 +2904,8 @@ "dest_name": "Aspetto di destinazione (opzionale)", "no_hubs": "Nessun hub configurato. Aggiungine uno per iniziare.", "no_rooms": "Nessuna stanza. Entra in una stanza per chattare.", + "available_rooms": "Stanze disponibili", + "join": "Entra", "no_room_selected": "Seleziona una stanza per vedere i messaggi.", "load_previous": "Carica messaggi precedenti", "rooms": "Stanze", diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json index e4000d71..3ba22289 100644 --- a/meshchatx/src/frontend/locales/nl.json +++ b/meshchatx/src/frontend/locales/nl.json @@ -2904,6 +2904,8 @@ "dest_name": "Bestemmingsaspect (optioneel)", "no_hubs": "Nog geen hubs geconfigureerd. Voeg er een toe om te beginnen.", "no_rooms": "Geen kamers. Word lid van een kamer om te chatten.", + "available_rooms": "Beschikbare kamers", + "join": "Deelnemen", "no_room_selected": "Selecteer een kamer om berichten te bekijken.", "load_previous": "Eerdere berichten laden", "rooms": "Kamers", diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json index 01ff4f23..00b39dd9 100644 --- a/meshchatx/src/frontend/locales/ru.json +++ b/meshchatx/src/frontend/locales/ru.json @@ -2904,6 +2904,8 @@ "dest_name": "Аспект назначения (необязательно)", "no_hubs": "Хабы ещё не настроены. Добавьте один, чтобы начать.", "no_rooms": "Нет комнат. Присоединитесь к комнате, чтобы общаться.", + "available_rooms": "Доступные комнаты", + "join": "Присоединиться", "no_room_selected": "Выберите комнату для просмотра сообщений.", "load_previous": "Загрузить более ранние сообщения", "rooms": "Комнаты", diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json index 9461de04..c9ec279b 100644 --- a/meshchatx/src/frontend/locales/zh.json +++ b/meshchatx/src/frontend/locales/zh.json @@ -2904,6 +2904,8 @@ "dest_name": "目标方面(可选)", "no_hubs": "尚未配置中心。添加一个以开始。", "no_rooms": "未加入任何房间。加入房间开始聊天。", + "available_rooms": "可用房间", + "join": "加入", "no_room_selected": "选择一个房间以查看消息。", "load_previous": "加载更早的消息", "rooms": "房间", diff --git a/tests/backend/test_rrc_protocol.py b/tests/backend/test_rrc_protocol.py index 93b92a83..6062eb4f 100644 --- a/tests/backend/test_rrc_protocol.py +++ b/tests/backend/test_rrc_protocol.py @@ -251,6 +251,42 @@ def test_hub_notice_updates_room_list(tmp_path): assert hub.available_rooms == {"lobby": "Main", "random": None} +def test_welcome_sends_list_when_auto_list_enabled(tmp_path): + manager = make_manager(tmp_path) + hub = manager.add_hub(bytes(range(16))) + hub.auto_list = True + calls = [] + hub.send_command = lambda text, room=None, record_local=True: calls.append(text) + + hub._handle_welcome({}) + + assert calls == ["/list"] + + +def test_set_auto_list_requests_list_immediately_when_already_connected(tmp_path): + """Enabling auto-list mid-session must not require a reconnect to take effect.""" + manager = make_manager(tmp_path) + hub = manager.add_hub(bytes(range(16))) + hub.welcomed = True + calls = [] + hub.send_command = lambda text, room=None, record_local=True: calls.append(text) + + hub.set_auto_list(True) + + assert calls == ["/list"] + + +def test_set_auto_list_does_not_list_before_hub_is_connected(tmp_path): + manager = make_manager(tmp_path) + hub = manager.add_hub(bytes(range(16))) + calls = [] + hub.send_command = lambda text, room=None, record_local=True: calls.append(text) + + hub.set_auto_list(True) + + assert calls == [] + + def test_history_is_persisted_and_reloaded(tmp_path): manager = make_manager(tmp_path) hub = manager.add_hub(bytes(range(16))) diff --git a/tests/frontend/RelayChatPage.test.js b/tests/frontend/RelayChatPage.test.js index ff2a67d5..15f9124f 100644 --- a/tests/frontend/RelayChatPage.test.js +++ b/tests/frontend/RelayChatPage.test.js @@ -130,6 +130,52 @@ describe("RelayChatPage.vue", () => { expect(wrapper.vm.selectedHubHash).toBe(HUB_HASH); }); + it("shows hub rooms discovered via auto-list that have not been joined yet", async () => { + axiosMock.get.mockImplementation((url) => { + if (url === "/api/v1/rrc/hubs") { + return Promise.resolve({ + data: { hubs: [makeHub({ available_rooms: { lobby: "Main", random: null } })] }, + }); + } + if (url === "/api/v1/rrc/servers") { + return Promise.resolve({ data: { hubs: [makeHostedHub()] } }); + } + if (url === "/api/v1/announces") { + return Promise.resolve({ data: { announces: [makeAnnounce()] } }); + } + return Promise.resolve({ data: {} }); + }); + const wrapper = mountPage(); + await vi.waitFor(() => expect(wrapper.vm.hubs.length).toBe(1)); + + // "lobby" is already joined (known_rooms), so only "random" is unjoined. + expect(wrapper.vm.availableRoomsFor(wrapper.vm.hubs[0])).toEqual([{ name: "random", topic: null }]); + expect(wrapper.text()).toContain("random"); + }); + + it("joins an unjoined available room via the API when clicked", async () => { + axiosMock.get.mockImplementation((url) => { + if (url === "/api/v1/rrc/hubs") { + return Promise.resolve({ + data: { hubs: [makeHub({ available_rooms: { random: null } })] }, + }); + } + if (url === "/api/v1/rrc/servers") { + return Promise.resolve({ data: { hubs: [makeHostedHub()] } }); + } + if (url === "/api/v1/announces") { + return Promise.resolve({ data: { announces: [makeAnnounce()] } }); + } + return Promise.resolve({ data: {} }); + }); + const wrapper = mountPage(); + await vi.waitFor(() => expect(wrapper.vm.hubs.length).toBe(1)); + + await wrapper.vm.joinAvailableRoom(wrapper.vm.hubs[0], "random"); + + expect(axiosMock.post).toHaveBeenCalledWith(`/api/v1/rrc/hubs/${HUB_HASH}/rooms`, { room: "random" }); + }); + it("loads messages and members when selecting a room", async () => { const wrapper = mountPage(); await vi.waitFor(() => expect(wrapper.vm.hubs.length).toBe(1)); diff --git a/typings/jnius/__init__.pyi b/typings/jnius/__init__.pyi new file mode 100644 index 00000000..f95df7b6 --- /dev/null +++ b/typings/jnius/__init__.pyi @@ -0,0 +1 @@ +# Optional Android dependency; stub for type checking only.