diff --git a/neode-ui/src/api/filebrowser-client.ts b/neode-ui/src/api/filebrowser-client.ts index 1d93e8f6..28f727a5 100644 --- a/neode-ui/src/api/filebrowser-client.ts +++ b/neode-ui/src/api/filebrowser-client.ts @@ -73,7 +73,15 @@ class FileBrowserClient { return h } + /** Ensure we're authenticated before making a request. Auto-logins if needed. */ + private async ensureAuth(): Promise { + if (this.token) return + const ok = await this.login() + if (!ok) throw new Error('FileBrowser authentication failed — please open Cloud to log in') + } + async listDirectory(path: string): Promise { + await this.ensureAuth() const safePath = sanitizePath(path) const res = await fetch(`${this.baseUrl}/api/resources${safePath}`, { headers: this.headers(), @@ -100,6 +108,7 @@ class FileBrowserClient { * Use this for img/video/audio src attributes and download links. */ async fetchBlobUrl(path: string): Promise { + await this.ensureAuth() const safePath = sanitizePath(path) const res = await fetch(`${this.baseUrl}/api/raw${safePath}`, { headers: this.headers(), @@ -125,6 +134,7 @@ class FileBrowserClient { } async upload(dirPath: string, file: File): Promise { + await this.ensureAuth() const sanitized = sanitizePath(dirPath) const safePath = sanitized.endsWith('/') ? sanitized : `${sanitized}/` const encodedName = encodeURIComponent(file.name) @@ -143,6 +153,7 @@ class FileBrowserClient { } async createFolder(parentPath: string, name: string): Promise { + await this.ensureAuth() const sanitized = sanitizePath(parentPath) const safePath = sanitized.endsWith('/') ? sanitized : `${sanitized}/` const sanitizedName = name.replace(/\.\./g, '').replace(/\//g, '') @@ -154,6 +165,7 @@ class FileBrowserClient { } async deleteItem(path: string): Promise { + await this.ensureAuth() const safePath = sanitizePath(path) const res = await fetch(`${this.baseUrl}/api/resources${safePath}`, { method: 'DELETE', diff --git a/neode-ui/src/views/Mesh.vue b/neode-ui/src/views/Mesh.vue index cb8a86cd..cb892f41 100644 --- a/neode-ui/src/views/Mesh.vue +++ b/neode-ui/src/views/Mesh.vue @@ -35,6 +35,54 @@ let pollInterval: ReturnType | null = null // The Public channel (always available on Meshcore) const publicChannel = { index: 0, name: 'Public' } +// Archipelago Channel — Tor-based messaging to all federated/peered nodes +const archChannelActive = ref(false) +const archMessages = ref>([]) +const archUnread = ref(0) +let archPollInterval: ReturnType | null = null + +function openArchChannel() { + activeChatPeer.value = null + activeChatChannel.value = null + archChannelActive.value = true + archUnread.value = 0 + mobileShowChat.value = true + loadArchMessages() + if (!archPollInterval) { + archPollInterval = setInterval(loadArchMessages, 15000) + } +} + +async function loadArchMessages() { + try { + const res = await rpcClient.getReceivedMessages() + archMessages.value = res.messages || [] + } catch { /* silent */ } +} + +async function sendArchMessage() { + if (!messageText.value.trim()) return + sendError.value = '' + try { + // Broadcast to all federated peers over Tor + const nodes = await rpcClient.federationListNodes() + const msg = messageText.value.trim() + let sent = 0 + for (const node of nodes.nodes) { + try { + await rpcClient.sendMessageToPeer(node.onion || node.did, msg) + sent++ + } catch { /* some peers may be offline */ } + } + messageText.value = '' + if (sent === 0) sendError.value = 'No peers reachable' + // Reload to see the message + setTimeout(loadArchMessages, 2000) + } catch (e) { + sendError.value = e instanceof Error ? e.message : 'Send failed' + } +} + const togglingOffGrid = ref(false) const peerSessionInfo = ref(null) @@ -244,21 +292,38 @@ onUnmounted(() => { // Active chat name for the header const activeChatName = computed(() => { + if (archChannelActive.value) return 'Archipelago' if (activeChatChannel.value) return activeChatChannel.value.name if (activeChatPeer.value) return activeChatPeer.value.advert_name return '' }) const activeChatSub = computed(() => { - if (activeChatChannel.value) return 'Mesh channel' + if (archChannelActive.value) return 'All nodes over Tor' + if (activeChatChannel.value) return 'Mesh radio' if (activeChatPeer.value) return truncatePubkey(activeChatPeer.value.pubkey_hex) return '' }) -const hasActiveChat = computed(() => !!activeChatPeer.value || !!activeChatChannel.value) +const hasActiveChat = computed(() => !!activeChatPeer.value || !!activeChatChannel.value || archChannelActive.value) // Messages filtered to the active chat const chatMessages = computed(() => { + if (archChannelActive.value) { + // Map Tor messages to mesh message format for rendering + return archMessages.value.map((m, i) => ({ + id: i, + peer_contact_id: -99, + peer_name: m.from_pubkey.slice(0, 12) + '...', + direction: 'received' as const, + plaintext: m.message, + timestamp: m.timestamp, + delivered: true, + encrypted: false, + message_type: undefined, + typed_payload: undefined, + })) + } if (activeChatChannel.value) { // Channel messages have negative contact_id = -(channel_index + 1) const chanId = -(activeChatChannel.value.index + 1) @@ -289,6 +354,7 @@ const sortedPeers = computed(() => { function openChat(peer: MeshPeer) { activeChatPeer.value = peer activeChatChannel.value = null + archChannelActive.value = false sendError.value = '' messageText.value = '' activeTab.value = 'chat' @@ -300,6 +366,7 @@ function openChat(peer: MeshPeer) { function openChannelChat(channel: { index: number; name: string }) { activeChatChannel.value = channel activeChatPeer.value = null + archChannelActive.value = false sendError.value = '' messageText.value = '' activeTab.value = 'chat' @@ -310,11 +377,18 @@ function openChannelChat(channel: { index: number; name: string }) { function closeChat() { activeChatPeer.value = null activeChatChannel.value = null + archChannelActive.value = false + if (archPollInterval) { clearInterval(archPollInterval); archPollInterval = null } mobileShowChat.value = false mesh.clearViewingChat() } async function handleSendMessage() { + if (archChannelActive.value) { + await sendArchMessage() + nextTick(() => scrollChatToBottom()) + return + } if (!activeChatPeer.value || !messageText.value.trim()) return sendError.value = '' try { @@ -498,7 +572,20 @@ function truncatePubkey(hex: string | null): string {
- + +
+
A
+
+
Archipelago
+
All nodes over Tor
+
+ {{ archUnread }} +
+
#
Public
-
Mesh channel
+
Mesh radio