@@ -2,7 +2,7 @@ const ONLINE_MS = 30 * 60 * 1000;
22
33const S = {
44 ws :null , connected :false , myNodeNum :null ,
5- nodes :{ } , channels :{ } , conversations :{ } , activeTab :null , unread :{ } ,
5+ nodes :{ } , channels :{ } , conversations :{ } , activeDM : null , activeChannel :null , unread :{ } ,
66 msgIndex :{ } ,
77 filter :'all' , startTime :null , selectedNode :null ,
88 uptimeTimer :null , wsTimer :null ,
@@ -83,8 +83,8 @@ function handle(msg) {
8383 S . everConnected = true ;
8484 try { localStorage . setItem ( 'mn_ever_connected' , '1' ) ; localStorage . setItem ( 'mn_last_ip' , msg . ip || '' ) ; } catch { }
8585 setConnUI ( true , msg . ip ) ;
86- { const sb = document . getElementById ( 'sendBtn' ) ; if ( sb ) sb . disabled = false ; }
87- { const ci = document . getElementById ( 'compInp' ) ; if ( ci ) ci . disabled = false ; }
86+ [ 'sendBtn' , 'sendBtn2' ] . forEach ( id => { const b = document . getElementById ( id ) ; if ( b ) b . disabled = false ; } ) ;
87+ [ 'compInp' , 'compInp2' ] . forEach ( id => { const i = document . getElementById ( id ) ; if ( i ) i . disabled = false ; } ) ;
8888 startUptime ( ) ;
8989 hideFirstRun ( ) ;
9090 updateIdentity ( ) ;
@@ -100,8 +100,8 @@ function handle(msg) {
100100 case 'disconnected' : case 'connectError' :
101101 S . connected = false ;
102102 setConnUI ( false , msg . type === 'connectError' ? msg . error : 'Disconnected' ) ;
103- { const sb = document . getElementById ( 'sendBtn' ) ; if ( sb ) sb . disabled = true ; }
104- { const ci = document . getElementById ( 'compInp' ) ; if ( ci ) ci . disabled = true ; }
103+ [ 'sendBtn' , 'sendBtn2' ] . forEach ( id => { const b = document . getElementById ( id ) ; if ( b ) b . disabled = true ; } ) ;
104+ [ 'compInp' , 'compInp2' ] . forEach ( id => { const i = document . getElementById ( id ) ; if ( i ) i . disabled = true ; } ) ;
105105 clearInterval ( S . uptimeTimer ) ;
106106 { const ut = document . getElementById ( 'uptimeText' ) ; if ( ut ) ut . textContent = '' ; }
107107 if ( msg . type === 'connectError' ) toast ( msg . error , 'err' ) ;
@@ -188,7 +188,8 @@ function handle(msg) {
188188 rxRssi :msg . rxRssi , rxSnr :msg . rxSnr ,
189189 hops :( msg . hopStart != null && msg . hopLimit != null ) ?msg . hopStart - msg . hopLimit :null
190190 } ) ;
191- if ( document . hidden || S . activeTab !== tabId ) {
191+ const activeId = isDM ?S . activeDM :S . activeChannel ;
192+ if ( document . hidden || activeId !== tabId ) {
192193 try { new Notification ( S . nodes [ msg . fromNum ] ?. name || 'Mesh' , { body :msg . text } ) ; } catch { }
193194 }
194195 break ;
@@ -239,7 +240,8 @@ function handle(msg) {
239240 m . ackTs = new Date ( ) ;
240241 toast ( m . statusLabel , 'err' ) ;
241242 }
242- if ( S . activeTab === ref . tabId ) renderMessages ( ) ;
243+ if ( ref . tabId . startsWith ( 'ch:' ) ) { if ( S . activeChannel === ref . tabId ) renderChannelThread ( ) ; }
244+ else { if ( S . activeDM === ref . tabId ) renderDMThread ( ) ; }
243245 }
244246 }
245247 break ;
@@ -283,10 +285,13 @@ function setFilter(f){
283285}
284286
285287// ── CONVERSATIONS ─────────────────────────────────────────────────
288+ // Messages (DMs) and Channels are two independent panels — each remembers
289+ // its own open conversation (S.activeDM / S.activeChannel) so switching
290+ // between them, or to another page entirely, never loses your place.
286291function ensureChTab ( ch ) {
287292 const id = 'ch:' + ch . index ;
288293 if ( ! S . conversations [ id ] ) { S . conversations [ id ] = [ ] ; S . unread [ id ] = 0 ; }
289- if ( ! S . activeTab ) setActiveTab ( id ) ;
294+ if ( ! S . activeChannel ) setActiveTab ( id ) ;
290295}
291296function ensureDMTab ( num ) {
292297 const id = 'dm:' + num ;
@@ -298,20 +303,21 @@ function pushConv(tabId,m){
298303 const idx = S . conversations [ tabId ] . length ;
299304 S . conversations [ tabId ] . push ( m ) ;
300305 if ( S . conversations [ tabId ] . length > 500 ) S . conversations [ tabId ] . shift ( ) ;
301- if ( S . activeTab !== tabId ) {
306+
307+ const isCh = tabId . startsWith ( 'ch:' ) ;
308+ const activeId = isCh ?S . activeChannel :S . activeDM ;
309+ if ( activeId !== tabId ) {
302310 S . unread [ tabId ] = ( S . unread [ tabId ] || 0 ) + 1 ;
303- // Bump right panel badge
304- if ( ! m . sys ) bumpMessageBadge ( ) ;
311+ if ( ! m . sys ) isCh ? bumpChannelBadge ( ) : bumpMessageBadge ( ) ;
305312 }
306- renderChatList ( ) ;
307- if ( S . activeTab === tabId ) renderMessages ( ) ;
308- // Refresh channels tab if open
309- if ( S . page === 'channels' ) renderChannelsList ( ) ;
313+ if ( isCh ) { renderChannelsList ( ) ; if ( activeId === tabId ) renderChannelThread ( ) ; }
314+ else { renderChatList ( ) ; if ( activeId === tabId ) renderDMThread ( ) ; }
310315 return idx ;
311316}
312317function setActiveTab ( id ) {
313- S . activeTab = id ; S . unread [ id ] = 0 ;
314- renderChatList ( ) ; renderMessages ( ) ; updateHeader ( ) ;
318+ S . unread [ id ] = 0 ;
319+ if ( id . startsWith ( 'ch:' ) ) { S . activeChannel = id ; renderChannelsList ( ) ; renderChannelThread ( ) ; }
320+ else { S . activeDM = id ; renderChatList ( ) ; renderDMThread ( ) ; }
315321}
316322function openDMWith ( num ) {
317323 ensureDMTab ( num ) ; renderChatList ( ) ; setActiveTab ( 'dm:' + num ) ;
@@ -323,43 +329,18 @@ function sysMsg(text){
323329 const id = keys . length ?keys [ 0 ] :'ch:0' ;
324330 if ( ! S . conversations [ id ] ) S . conversations [ id ] = [ ] ;
325331 S . conversations [ id ] . push ( { sys :true , text, ts :new Date ( ) } ) ;
326- if ( S . activeTab === id ) renderMessages ( ) ;
332+ if ( id . startsWith ( 'ch:' ) ) { if ( S . activeChannel === id ) renderChannelThread ( ) ; }
333+ else { if ( S . activeDM === id ) renderDMThread ( ) ; }
327334}
328335
329- // ── CONVERSATION LIST (channels + open direct messages) ───────────
336+ // ── CONVERSATION LIST (direct messages only) ───────────── ───────────
330337function renderChatList ( ) {
331338 const el = document . getElementById ( 'chatList' ) ;
332339 if ( ! el ) return ;
333340 let html = '' ;
334341
335- // Channels
336- const chans = Object . values ( S . channels ) . sort ( ( a , b ) => a . index - b . index ) ;
337- if ( chans . length ) {
338- html += '<div class="list-hdr">Channels <span class="list-hdr-count">' + chans . length + '</span></div>' ;
339- chans . forEach ( ch => {
340- const id = 'ch:' + ch . index ;
341- const msgs = ( S . conversations [ id ] || [ ] ) . filter ( m => ! m . sys ) ;
342- const last = msgs [ msgs . length - 1 ] ;
343- const unread = S . unread [ id ] || 0 ;
344- const active = S . activeTab === id ;
345- const preview = last ?( last . mine ?'You: ' + esc ( last . text ) :esc ( last . fromName || '?' ) + ': ' + esc ( last . text ) ) :'No messages yet' ;
346- const tstr = last ?timeStr ( last . ts ) :'' ;
347- html += `<div class="chat-row${ active ?' active' :'' } " onclick="setActiveTab('${ id } ')">
348- <div class="avatar ch-av"><span class="icon sm">campaign</span></div>
349- <div class="row-info">
350- <div class="row-name">${ esc ( ch . name ) } <span style="font-size:10px;color:var(--label-3);font-weight:400">${ ch . role === 'PRIMARY' ?'Primary' :'' } </span></div>
351- <div class="row-preview">${ last && last . mine ?tickPreview ( last . status ) :'' } ${ preview } </div>
352- </div>
353- <div class="row-meta">
354- <span class="row-time">${ tstr } </span>
355- ${ unread ?`<span class="unread-badge">${ unread } </span>` :'' }
356- </div>
357- </div>` ;
358- } ) ;
359- }
360-
361- // Direct messages — only conversations that actually exist (the full node
362- // roster now lives on the Peers page)
342+ // Direct messages only — channels live on their own Channels page now.
343+ // Only conversations that actually exist (the full node roster lives on Peers).
363344 let dms = Object . keys ( S . conversations )
364345 . filter ( k => k . startsWith ( 'dm:' ) )
365346 . map ( k => ( { id :k , num :parseInt ( k . slice ( 3 ) ) , node :S . nodes [ parseInt ( k . slice ( 3 ) ) ] || null } ) ) ;
@@ -371,15 +352,14 @@ function renderChatList(){
371352 } ) ;
372353
373354 if ( dms . length ) {
374- html += `<div class="list-hdr">Direct messages <span class="list-hdr-count">${ dms . length } </span></div>` ;
375355 dms . forEach ( d => {
376356 const n = d . node || { num :d . num , name :numToId ( d . num ) , shortName :'???' } ;
377357 const isMe = n . num === S . myNodeNum ;
378358 const online = d . node ?isOnline ( d . node ) :false ;
379359 const msgs = ( S . conversations [ d . id ] || [ ] ) . filter ( m => ! m . sys ) ;
380360 const last = msgs [ msgs . length - 1 ] ;
381361 const unread = S . unread [ d . id ] || 0 ;
382- const active = S . activeTab === d . id ;
362+ const active = S . activeDM === d . id ;
383363 const sc = sigColor ( n . rssi ) ;
384364 const preview = last ?( last . mine ?'You: ' + esc ( last . text ) :esc ( last . text ) )
385365 :( n . lastHeard ?'Last heard ' + timeAgo ( n . lastHeard ) :'No messages yet' ) ;
@@ -404,8 +384,8 @@ function renderChatList(){
404384 if ( ! html ) {
405385 html = `<div class="list-empty">
406386 <span class="icon xl">forum</span>
407- <div class="list-empty-title">No conversations yet</div>
408- <div class="list-empty-sub">${ S . connected ?'Channels appear once your node finishes loading. Open Peers to start a direct message.' :'Connect to your node to load channels and peers .' } </div>
387+ <div class="list-empty-title">No direct messages yet</div>
388+ <div class="list-empty-sub">${ S . connected ?'Open Peers to start a direct message.' :'Connect to your node, then open Peers to start a direct message .' } </div>
409389 </div>` ;
410390 }
411391
@@ -499,54 +479,74 @@ function tickPreview(status){
499479}
500480
501481// ── CHAT HEADER ───────────────────────────────────────────────────
502- function updateHeader ( ) {
482+ // Messages (DMs) and Channels each have their own header/feed/composer DOM,
483+ // so each gets its own render pair — but both build bubbles the same way.
484+ function renderDMHeader ( ) {
503485 const nameEl = document . getElementById ( 'chName' ) ;
504486 const subEl = document . getElementById ( 'chSub' ) ;
505487 const aviEl = document . getElementById ( 'chAvi' ) ;
506488 const destLabel = document . getElementById ( 'composeDestLabel' ) ;
489+ const id = S . activeDM ;
507490
508- if ( ! S . activeTab ) {
491+ if ( ! id ) {
509492 nameEl . textContent = 'Select a conversation' ;
510- subEl . textContent = 'Pick a channel or peer on the left' ;
493+ subEl . textContent = 'Pick a peer on the left' ;
511494 destLabel . textContent = 'No conversation selected' ;
495+ aviEl . innerHTML = '<span class="icon" style="color:var(--label-3)">forum</span>' ;
512496 return ;
513497 }
514- if ( S . activeTab . startsWith ( 'ch:' ) ) {
515- const idx = parseInt ( S . activeTab . slice ( 3 ) ) ;
516- const ch = S . channels [ idx ] ;
517- nameEl . textContent = ch ?ch . name :'Channel ' + idx ;
518- aviEl . innerHTML = '<span class="icon" style="color:var(--blue)">campaign</span>' ;
519- destLabel . textContent = ( ch ?ch . name :'Channel ' + idx ) + ' — all nodes' ;
520- const nodeCount = Object . keys ( S . nodes ) . length ;
521- subEl . textContent = ( ch ?. role === 'PRIMARY' ?'Primary channel' :'Secondary channel' ) + ' · broadcast to ' + nodeCount + ' known node' + ( nodeCount !== 1 ?'s' :'' ) ;
522- } else {
523- const num = parseInt ( S . activeTab . slice ( 3 ) ) ;
524- const n = S . nodes [ num ] ;
525- nameEl . textContent = n ?n . name :numToId ( num ) ;
526- aviEl . innerHTML = '<span class="icon" style="color:var(--label-3)">person</span>' ;
527- destLabel . textContent = ( n ?n . name :numToId ( num ) ) + ' — direct message' ;
528- const online = n && isOnline ( n ) ;
529- subEl . textContent = n ?( online ?'Online now' :'Last seen ' + ( n . lastHeard ?timeAgo ( n . lastHeard ) :'unknown' ) ) :numToId ( num ) ;
498+ const num = parseInt ( id . slice ( 3 ) ) ;
499+ const n = S . nodes [ num ] ;
500+ nameEl . textContent = n ?n . name :numToId ( num ) ;
501+ aviEl . innerHTML = '<span class="icon" style="color:var(--label-3)">person</span>' ;
502+ destLabel . textContent = ( n ?n . name :numToId ( num ) ) + ' — direct message' ;
503+ const online = n && isOnline ( n ) ;
504+ subEl . textContent = n ?( online ?'Online now' :'Last seen ' + ( n . lastHeard ?timeAgo ( n . lastHeard ) :'unknown' ) ) :numToId ( num ) ;
505+ }
506+ function renderChannelHeader ( ) {
507+ const nameEl = document . getElementById ( 'chName2' ) ;
508+ const subEl = document . getElementById ( 'chSub2' ) ;
509+ const aviEl = document . getElementById ( 'chAvi2' ) ;
510+ const destLabel = document . getElementById ( 'composeDestLabel2' ) ;
511+ const id = S . activeChannel ;
512+
513+ if ( ! id ) {
514+ nameEl . textContent = 'Select a channel' ;
515+ subEl . textContent = 'Pick a channel on the left' ;
516+ destLabel . textContent = 'No channel selected' ;
517+ aviEl . innerHTML = '<span class="icon" style="color:var(--label-3)">hub</span>' ;
518+ return ;
530519 }
520+ const idx = parseInt ( id . slice ( 3 ) ) ;
521+ const ch = S . channels [ idx ] ;
522+ nameEl . textContent = ch ?ch . name :'Channel ' + idx ;
523+ aviEl . innerHTML = '<span class="icon" style="color:var(--accent)">campaign</span>' ;
524+ destLabel . textContent = ( ch ?ch . name :'Channel ' + idx ) + ' — all nodes' ;
525+ const nodeCount = Object . keys ( S . nodes ) . length ;
526+ subEl . textContent = ( ch ?. role === 'PRIMARY' ?'Primary channel' :'Secondary channel' ) + ' · broadcast to ' + nodeCount + ' known node' + ( nodeCount !== 1 ?'s' :'' ) ;
531527}
532528
533529// ── MESSAGES ──────────────────────────────────────────────────────
534- function renderMessages ( ) {
535- const feed = document . getElementById ( 'msgFeed' ) ;
536- const msgs = S . activeTab ?( S . conversations [ S . activeTab ] || [ ] ) :[ ] ;
530+ function renderDMThread ( ) {
531+ renderDMHeader ( ) ;
532+ const id = S . activeDM ;
533+ let title = 'Meshnatter' , sub = 'Connect to get started' ;
534+ if ( id ) { const num = parseInt ( id . slice ( 3 ) ) ; title = S . nodes [ num ] ?. name || numToId ( num ) ; sub = 'No messages — say something!' ; }
535+ renderThreadInto ( 'msgFeed' , id , title , sub ) ;
536+ }
537+ function renderChannelThread ( ) {
538+ renderChannelHeader ( ) ;
539+ const id = S . activeChannel ;
540+ let title = 'Meshnatter' , sub = 'Select a channel on the left to see its broadcasts' ;
541+ if ( id ) { const idx = parseInt ( id . slice ( 3 ) ) ; title = S . channels [ idx ] ?. name || 'Channel' ; sub = 'No messages yet' ; }
542+ renderThreadInto ( 'msgFeed2' , id , title , sub ) ;
543+ }
544+ function renderThreadInto ( feedId , tabId , emptyTitle , emptySub ) {
545+ const feed = document . getElementById ( feedId ) ;
546+ const msgs = tabId ?( S . conversations [ tabId ] || [ ] ) :[ ] ;
537547
538548 if ( ! msgs . length ) {
539- let title = '' , sub = '' ;
540- if ( S . activeTab ?. startsWith ( 'ch:' ) ) {
541- const idx = parseInt ( S . activeTab . slice ( 3 ) ) ;
542- title = S . channels [ idx ] ?. name || 'Channel' ; sub = 'No messages yet' ;
543- } else if ( S . activeTab ?. startsWith ( 'dm:' ) ) {
544- const num = parseInt ( S . activeTab . slice ( 3 ) ) ;
545- title = S . nodes [ num ] ?. name || numToId ( num ) ; sub = 'No messages — say something!' ;
546- } else {
547- title = 'Meshnatter' ; sub = 'Connect to get started' ;
548- }
549- feed . innerHTML = `<div class="msg-empty"><span class="icon xl">forum</span><div class="msg-empty-title">${ esc ( title ) } </div><div class="msg-empty-sub">${ esc ( sub ) } </div></div>` ;
549+ feed . innerHTML = `<div class="msg-empty"><span class="icon xl">forum</span><div class="msg-empty-title">${ esc ( emptyTitle ) } </div><div class="msg-empty-sub">${ esc ( emptySub ) } </div></div>` ;
550550 return ;
551551 }
552552
@@ -582,7 +582,7 @@ function renderMessages(){
582582 maxretransmit :`<span class="icon sm">error_outline</span> Max Retransmission Reached` ,
583583 nack : `<span class="icon sm">error_outline</span> ` + ( m . statusLabel || m . statusError || 'Failed' ) ,
584584 } ;
585- statusLine = `<div class="msg-status-line ${ st } " onclick="showMessageDetails('${ S . activeTab } ',${ msgs . indexOf ( m ) } )" title="Message details">${ labels [ st ] || '' } </div>` ;
585+ statusLine = `<div class="msg-status-line ${ st } " onclick="showMessageDetails('${ tabId } ',${ msgs . indexOf ( m ) } )" title="Message details">${ labels [ st ] || '' } </div>` ;
586586 }
587587
588588 // Incoming signal info
@@ -640,13 +640,15 @@ function showMessageDetails(tabId, idx){
640640 document . getElementById ( 'mdOv' ) . classList . add ( 'show' ) ;
641641}
642642
643- function sendMsg ( ) {
644- const inp = document . getElementById ( 'compInp' ) ;
643+ function sendMsg ( kind ) {
644+ const isCh = kind === 'ch' ;
645+ const activeId = isCh ?S . activeChannel :S . activeDM ;
646+ const inp = document . getElementById ( isCh ?'compInp2' :'compInp' ) ;
645647 const text = inp . value . trim ( ) ;
646- if ( ! text || ! S . connected || ! S . activeTab ) return ;
648+ if ( ! text || ! S . connected || ! activeId ) return ;
647649 let destinationNum = null , channelIndex = 0 ;
648- if ( S . activeTab . startsWith ( 'ch:' ) ) channelIndex = parseInt ( S . activeTab . slice ( 3 ) ) ;
649- else destinationNum = parseInt ( S . activeTab . slice ( 3 ) ) ;
650+ if ( isCh ) channelIndex = parseInt ( activeId . slice ( 3 ) ) ;
651+ else destinationNum = parseInt ( activeId . slice ( 3 ) ) ;
650652 wsSend ( { type :'sendMessage' , text, destinationNum, channelIndex} ) ;
651653 inp . value = '' ; inp . focus ( ) ;
652654}
@@ -783,18 +785,18 @@ function toggleConn(){
783785 wsSend ( { type :'disconnect' } ) ;
784786 S . connected = false ; S . myNodeNum = null ;
785787 S . nodes = { } ; S . channels = { } ; S . conversations = { } ; S . unread = { } ;
786- S . activeTab = null ; S . msgIndex = { } ;
788+ S . activeDM = null ; S . activeChannel = null ; S . msgIndex = { } ;
787789 Object . values ( markers ) . forEach ( m => map . removeLayer ( m ) ) ;
788790 for ( const k in markers ) delete markers [ k ] ;
789791 setConnUI ( false , 'Disconnected' ) ;
790- document . getElementById ( 'sendBtn' ) . disabled = true ;
791- document . getElementById ( 'compInp' ) . disabled = true ;
792+ [ 'sendBtn' , 'sendBtn2' ] . forEach ( id => { document . getElementById ( id ) . disabled = true ; } ) ;
793+ [ 'compInp' , 'compInp2' ] . forEach ( id => { document . getElementById ( id ) . disabled = true ; } ) ;
792794 clearInterval ( S . uptimeTimer ) ;
793795 document . getElementById ( 'uptimeText' ) . textContent = '' ;
794796 S . deviceConfig = null ; S . configDirty = false ;
795- renderChatList ( ) ; renderStats ( ) ; renderMap ( ) ; renderMessages ( ) ; renderPeers ( ) ;
797+ renderChatList ( ) ; renderStats ( ) ; renderMap ( ) ; renderDMThread ( ) ; renderChannelThread ( ) ; renderPeers ( ) ;
796798 renderChannelsList ( ) ; closeNodeDetail ( ) ;
797- updateHeader ( ) ; updateIdentity ( ) ;
799+ updateIdentity ( ) ;
798800 if ( S . page === 'config' ) renderConfigPage ( ) ;
799801 } else {
800802 const ip = document . getElementById ( 'nodeIp' ) . value . trim ( ) ;
@@ -1308,7 +1310,7 @@ function renderChannelsList() {
13081310 const msgs = ( S . conversations [ tabId ] || [ ] ) . filter ( m => ! m . sys ) ;
13091311 const last = msgs [ msgs . length - 1 ] ;
13101312 const unread = S . unread [ tabId ] || 0 ;
1311- const active = S . activeTab === tabId ;
1313+ const active = S . activeChannel === tabId ;
13121314 const nodeCount = Object . values ( S . nodes ) . length ;
13131315 const onlineCount = Object . values ( S . nodes ) . filter ( isOnline ) . length ;
13141316 const lastPreview = last
@@ -1334,8 +1336,7 @@ function renderChannelsList() {
13341336
13351337function openChannel ( tabId ) {
13361338 setActiveTab ( tabId ) ;
1337- navigate ( 'messages' ) ;
1338- const inp = document . getElementById ( 'compInp' ) ;
1339+ const inp = document . getElementById ( 'compInp2' ) ;
13391340 if ( inp ) inp . focus ( ) ;
13401341}
13411342
@@ -1382,11 +1383,11 @@ function init() {
13821383 navigate ( page ) ;
13831384
13841385 renderChatList ( ) ;
1385- renderMessages ( ) ;
1386+ renderDMThread ( ) ;
13861387 renderPeers ( ) ;
13871388 renderChannelsList ( ) ;
1389+ renderChannelThread ( ) ;
13881390 updateIdentity ( ) ;
1389- updateHeader ( ) ;
13901391
13911392 // Guided first-run connect card, or the compact reconnect control for returning users
13921393 if ( ! S . everConnected ) showFirstRun ( ) ;
0 commit comments