mirror of
https://github.com/Quad4-Software/MeshChatX.git
synced 2026-08-18 09:49:09 -04:00
feat(relay_chat): improve room management by adding available rooms display and joining functionality
This commit is contained in:
parent
6b8c341688
commit
48cf5737c4
15 changed files with 172 additions and 11 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -236,13 +236,46 @@
|
|||
</span>
|
||||
</li>
|
||||
<li
|
||||
v-if="orderedRoomsFor(hub).length === 0"
|
||||
v-if="orderedRoomsFor(hub).length === 0 && availableRoomsFor(hub).length === 0"
|
||||
class="px-2.5 py-1 text-xs text-sem-fg-muted"
|
||||
>
|
||||
{{ $t("relay_chat.no_rooms") }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="availableRoomsFor(hub).length > 0" class="space-y-0.5">
|
||||
<div
|
||||
class="px-2.5 pt-1 text-[10px] font-semibold uppercase tracking-wide text-sem-fg-muted"
|
||||
>
|
||||
{{ $t("relay_chat.available_rooms") }}
|
||||
</div>
|
||||
<ul class="space-y-0.5">
|
||||
<li
|
||||
v-for="availableRoom in availableRoomsFor(hub)"
|
||||
:key="availableRoom.name"
|
||||
:title="availableRoom.topic || ''"
|
||||
class="flex items-center justify-between gap-2 rounded-lg px-2.5 py-1.5 text-sm cursor-pointer text-sem-fg-muted transition-colors hover:bg-sem-surface/60 dark:hover:bg-sem-surface/30"
|
||||
@click="joinAvailableRoom(hub, availableRoom.name)"
|
||||
>
|
||||
<span class="flex min-w-0 items-center gap-1.5">
|
||||
<MaterialDesignIcon
|
||||
icon-name="pound"
|
||||
class="size-3.5 shrink-0 opacity-40"
|
||||
/>
|
||||
<span class="truncate">{{ availableRoom.name }}</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
:class="btnIconSm"
|
||||
:title="$t('relay_chat.join')"
|
||||
@click.stop="joinAvailableRoom(hub, availableRoom.name)"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="plus" class="size-3.5" />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form class="flex gap-1.5" @submit.prevent="joinRoom(hub)">
|
||||
<input
|
||||
v-model="joinRoomName"
|
||||
|
|
@ -1388,6 +1421,16 @@ export default {
|
|||
}
|
||||
return hub.known_rooms;
|
||||
},
|
||||
availableRoomsFor(hub) {
|
||||
if (!hub || !hub.available_rooms || typeof hub.available_rooms !== "object") {
|
||||
return [];
|
||||
}
|
||||
const known = new Set(this.orderedRoomsFor(hub));
|
||||
return Object.entries(hub.available_rooms)
|
||||
.filter(([name]) => !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() {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -2904,6 +2904,8 @@
|
|||
"dest_name": "Аспект назначения (необязательно)",
|
||||
"no_hubs": "Хабы ещё не настроены. Добавьте один, чтобы начать.",
|
||||
"no_rooms": "Нет комнат. Присоединитесь к комнате, чтобы общаться.",
|
||||
"available_rooms": "Доступные комнаты",
|
||||
"join": "Присоединиться",
|
||||
"no_room_selected": "Выберите комнату для просмотра сообщений.",
|
||||
"load_previous": "Загрузить более ранние сообщения",
|
||||
"rooms": "Комнаты",
|
||||
|
|
|
|||
|
|
@ -2904,6 +2904,8 @@
|
|||
"dest_name": "目标方面(可选)",
|
||||
"no_hubs": "尚未配置中心。添加一个以开始。",
|
||||
"no_rooms": "未加入任何房间。加入房间开始聊天。",
|
||||
"available_rooms": "可用房间",
|
||||
"join": "加入",
|
||||
"no_room_selected": "选择一个房间以查看消息。",
|
||||
"load_previous": "加载更早的消息",
|
||||
"rooms": "房间",
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
1
typings/jnius/__init__.pyi
Normal file
1
typings/jnius/__init__.pyi
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Optional Android dependency; stub for type checking only.
|
||||
Loading…
Add table
Add a link
Reference in a new issue