diff --git a/frontend/dist/assets/index-DVwayj9K.js b/frontend/dist/assets/index-CBdmGStv.js similarity index 99% rename from frontend/dist/assets/index-DVwayj9K.js rename to frontend/dist/assets/index-CBdmGStv.js index 81046ab2d..e9fda9b59 100644 --- a/frontend/dist/assets/index-DVwayj9K.js +++ b/frontend/dist/assets/index-CBdmGStv.js @@ -42,7 +42,7 @@ Error generating stack: `+b.message+` No neighbors reported`;const o=[...a].sort((s,u)=>u.snr-s.snr),r=[`Neighbors (${o.length})`];for(const s of o){const u=s.name||s.pubkey_prefix,l=s.snr>=0?`+${s.snr.toFixed(1)}`:s.snr.toFixed(1);r.push(`${u}, ${l} dB [${st(s.last_heard_seconds)} ago]`)}return r.join(` `)}function mk(a){if(a.length===0)return`ACL No ACL entries`;const o=[`ACL (${a.length})`];for(const r of a){const s=r.name||r.pubkey_prefix;o.push(`${s}: ${r.permission_name}`)}return o.join(` -`)}function Hn(a,o,r,s=0){const u=Math.floor(Date.now()/1e3);return{id:-Date.now()-s,type:"PRIV",conversation_key:a,text:o,sender_timestamp:u,received_at:u,path_len:null,txt_type:0,signature:null,outgoing:r,acked:1}}function hk(a,o,r){const[s,u]=E.useState(!1);E.useEffect(()=>{u(!1)},[a==null?void 0:a.id]);const l=E.useMemo(()=>{if(!a||a.type!=="contact")return!1;const h=o.find(d=>d.public_key===a.id);return(h==null?void 0:h.type)===$g},[a,o]),m=E.useCallback(async h=>{if(!(!a||a.type!=="contact")&&l)try{const d=await We.requestTelemetry(a.id,h),g=Hn(a.id,dk(d),!1,0),y=Hn(a.id,pk(d.neighbors),!1,1),f=Hn(a.id,mk(d.acl),!1,2);r(z=>[...z,g,y,f]),u(!0)}catch(d){const g=Hn(a.id,`Telemetry request failed: ${d instanceof Error?d.message:"Unknown error"}`,!1,0);r(y=>[...y,g])}},[a,l,r]),c=E.useCallback(async h=>{if(!a||a.type!=="contact"||!l||!s)return;const d=Hn(a.id,`> ${h}`,!0,0);r(g=>[...g,d]);try{const g=await We.sendRepeaterCommand(a.id,h),y=Hn(a.id,g.response,!1,1);g.sender_timestamp&&(y.sender_timestamp=g.sender_timestamp),r(f=>[...f,y])}catch(g){const y=Hn(a.id,`Command failed: ${g instanceof Error?g.message:"Unknown error"}`,!1,1);r(f=>[...f,y])}},[a,l,s,r]);return{repeaterLoggedIn:s,activeContactIsRepeater:l,handleTelemetryRequest:m,handleRepeaterCommand:c}}const gk=12;function Br(a){return a.slice(0,gk)}function Gg(a,o){return!a||!o?!1:Br(a)===Br(o)}function Yn(a,o){return a||Br(o)}const wu="remoteterm-lastMessageTime";function Qg(a){try{const o=localStorage.getItem(a);return o?JSON.parse(o):{}}catch{return{}}}function yk(a,o){try{localStorage.setItem(a,JSON.stringify(o))}catch{}}function Qm(){return Qg(wu)}function ru(a,o){const r=Qg(wu);return(!r[a]||o>r[a])&&(r[a]=o,yk(wu,r)),r}function Oa(a,o){return a==="channel"?`channel-${o}`:`contact-${Br(o)}`}function bk(a,o,r){const[s,u]=E.useState({}),[l,m]=E.useState(Qm),c=E.useRef(new Set),h=E.useRef(new Set);E.useEffect(()=>{const z=a.filter(v=>!c.current.has(v.key)),w=o.filter(v=>v.public_key&&!h.current.has(v.public_key));if(z.length===0&&w.length===0)return;z.forEach(v=>c.current.add(v.key)),w.forEach(v=>h.current.add(v.public_key)),(async()=>{const v=[...z.map(j=>({type:"CHAN",conversation_key:j.key})),...w.map(j=>({type:"PRIV",conversation_key:j.public_key}))];if(v.length!==0)try{const j=await We.getMessagesBulk(v,100),q={},x={};for(const _ of z){const C=j[`CHAN:${_.key}`]||[];if(C.length>0){const D=Oa("channel",_.key),R=_.last_read_at||0,F=C.filter(P=>!P.outgoing&&P.received_at>R).length;F>0&&(q[D]=F);const M=Math.max(...C.map(P=>P.received_at));x[D]=M,ru(D,M)}}for(const _ of w){const C=j[`PRIV:${_.public_key}`]||[];if(C.length>0){const D=Oa("contact",_.public_key),R=_.last_read_at||0,F=C.filter(P=>!P.outgoing&&P.received_at>R).length;F>0&&(q[D]=F);const M=Math.max(...C.map(P=>P.received_at));x[D]=M,ru(D,M)}}Object.keys(q).length>0&&u(_=>({..._,...q})),m(Qm())}catch(j){console.error("Failed to fetch messages bulk:",j)}})()},[a,o]),E.useEffect(()=>{if(r&&r.type!=="raw"){const z=Oa(r.type,r.id);u(w=>{if(w[z]){const k={...w};return delete k[z],k}return w}),r.type==="channel"?We.markChannelRead(r.id).catch(w=>{console.error("Failed to mark channel as read on server:",w)}):r.type==="contact"&&We.markContactRead(r.id).catch(w=>{console.error("Failed to mark contact as read on server:",w)})}},[r]);const d=E.useCallback(z=>{u(w=>({...w,[z]:(w[z]||0)+1}))},[]),g=E.useCallback(()=>{u({}),We.markAllRead().catch(z=>{console.error("Failed to mark all as read on server:",z)})},[]),y=E.useCallback(z=>{if(z.type==="raw")return;const w=Oa(z.type,z.id);u(k=>{if(k[w]){const v={...k};return delete v[w],v}return k}),z.type==="channel"?We.markChannelRead(z.id).catch(k=>{console.error("Failed to mark channel as read on server:",k)}):z.type==="contact"&&We.markContactRead(z.id).catch(k=>{console.error("Failed to mark contact as read on server:",k)})},[]),f=E.useCallback(z=>{let w=null;if(z.type==="CHAN"&&z.conversation_key?w=Oa("channel",z.conversation_key):z.type==="PRIV"&&z.conversation_key&&(w=Oa("contact",z.conversation_key)),w){const k=z.received_at||Math.floor(Date.now()/1e3),v=ru(w,k);m(v)}},[]);return{unreadCounts:s,lastMessageTimes:l,incrementUnread:d,markAllRead:g,markConversationRead:y,trackNewMessage:f}}const Zs=200;function ou(a){return`${a.type}-${a.conversation_key}-${a.text}-${a.sender_timestamp}`}function fk(a){const[o,r]=E.useState([]),[s,u]=E.useState(!1),[l,m]=E.useState(!1),[c,h]=E.useState(!1),d=E.useRef(new Set),g=E.useCallback(async(w=!1)=>{if(!a||a.type==="raw"){r([]),h(!1);return}w&&u(!0);try{const k=await We.getMessages({type:a.type==="channel"?"CHAN":"PRIV",conversation_key:a.id,limit:Zs});r(k),d.current.clear();for(const v of k)d.current.add(ou(v));h(k.length>=Zs)}catch(k){console.error("Failed to fetch messages:",k)}finally{w&&u(!1)}},[a]),y=E.useCallback(async()=>{if(!(!a||a.type==="raw"||l||!c)){m(!0);try{const w=await We.getMessages({type:a.type==="channel"?"CHAN":"PRIV",conversation_key:a.id,limit:Zs,offset:o.length});if(w.length>0){r(k=>[...k,...w]);for(const k of w)d.current.add(ou(k))}h(w.length>=Zs)}catch(w){console.error("Failed to fetch older messages:",w)}finally{m(!1)}}},[a,l,c,o.length]);E.useEffect(()=>{g(!0)},[g]);const f=E.useCallback(w=>{const k=ou(w);if(d.current.has(k))return console.debug("Duplicate message content ignored:",k.slice(0,50)),!1;if(d.current.add(k),d.current.size>1e3){const v=Array.from(d.current);d.current=new Set(v.slice(-500))}return r(v=>v.some(j=>j.id===w.id)?v:[...v,w]),!0},[]),z=E.useCallback((w,k)=>{r(v=>{const j=v.findIndex(q=>q.id===w);if(j>=0){const q=[...v];return q[j]={...v[j],acked:k},q}return v})},[]);return{messages:o,messagesLoading:s,loadingOlder:l,hasOlderMessages:c,setMessages:r,fetchMessages:g,fetchOlderMessages:y,addMessageIfNew:f,updateMessageAck:z}}/** +`)}function Hn(a,o,r,s=0){const u=Math.floor(Date.now()/1e3);return{id:-Date.now()-s,type:"PRIV",conversation_key:a,text:o,sender_timestamp:u,received_at:u,path_len:null,txt_type:0,signature:null,outgoing:r,acked:1}}function hk(a,o,r){const[s,u]=E.useState(!1);E.useEffect(()=>{u(!1)},[a==null?void 0:a.id]);const l=E.useMemo(()=>{if(!a||a.type!=="contact")return!1;const h=o.find(d=>d.public_key===a.id);return(h==null?void 0:h.type)===$g},[a,o]),m=E.useCallback(async h=>{if(!(!a||a.type!=="contact")&&l)try{const d=await We.requestTelemetry(a.id,h),g=Hn(a.id,dk(d),!1,0),y=Hn(a.id,pk(d.neighbors),!1,1),f=Hn(a.id,mk(d.acl),!1,2);r(z=>[...z,g,y,f]),u(!0)}catch(d){const g=Hn(a.id,`Telemetry request failed: ${d instanceof Error?d.message:"Unknown error"}`,!1,0);r(y=>[...y,g])}},[a,l,r]),c=E.useCallback(async h=>{if(!a||a.type!=="contact"||!l||!s)return;const d=Hn(a.id,`> ${h}`,!0,0);r(g=>[...g,d]);try{const g=await We.sendRepeaterCommand(a.id,h),y=Hn(a.id,g.response,!1,1);g.sender_timestamp&&(y.sender_timestamp=g.sender_timestamp),r(f=>[...f,y])}catch(g){const y=Hn(a.id,`Command failed: ${g instanceof Error?g.message:"Unknown error"}`,!1,1);r(f=>[...f,y])}},[a,l,s,r]);return{repeaterLoggedIn:s,activeContactIsRepeater:l,handleTelemetryRequest:m,handleRepeaterCommand:c}}const gk=12;function Br(a){return a.slice(0,gk)}function Gg(a,o){return!a||!o?!1:Br(a)===Br(o)}function Yn(a,o){return a||Br(o)}const wu="remoteterm-lastMessageTime";function Qg(a){try{const o=localStorage.getItem(a);return o?JSON.parse(o):{}}catch{return{}}}function yk(a,o){try{localStorage.setItem(a,JSON.stringify(o))}catch{}}function Qm(){return Qg(wu)}function ru(a,o){const r=Qg(wu);return(!r[a]||o>r[a])&&(r[a]=o,yk(wu,r)),r}function Oa(a,o){return a==="channel"?`channel-${o}`:`contact-${Br(o)}`}function bk(a,o,r){const[s,u]=E.useState({}),[l,m]=E.useState(Qm),c=E.useRef(new Set),h=E.useRef(new Set);E.useEffect(()=>{const z=a.filter(v=>!c.current.has(v.key)),w=o.filter(v=>v.public_key&&!h.current.has(v.public_key));if(z.length===0&&w.length===0)return;z.forEach(v=>c.current.add(v.key)),w.forEach(v=>h.current.add(v.public_key)),(async()=>{const v=[...z.map(j=>({type:"CHAN",conversation_key:j.key})),...w.map(j=>({type:"PRIV",conversation_key:j.public_key}))];if(v.length!==0)try{const j=await We.getMessagesBulk(v,100),q={},x={};for(const _ of z){const C=j[`CHAN:${_.key}`]||[];if(C.length>0){const D=Oa("channel",_.key),R=_.last_read_at||0,F=C.filter(P=>!P.outgoing&&P.received_at>R).length;F>0&&(q[D]=F);const M=Math.max(...C.map(P=>P.received_at));x[D]=M,ru(D,M)}}for(const _ of w){const C=j[`PRIV:${_.public_key}`]||[];if(C.length>0){const D=Oa("contact",_.public_key),R=_.last_read_at||0,F=C.filter(P=>!P.outgoing&&P.received_at>R).length;F>0&&(q[D]=F);const M=Math.max(...C.map(P=>P.received_at));x[D]=M,ru(D,M)}}Object.keys(q).length>0&&u(_=>({..._,...q})),m(Qm())}catch(j){console.error("Failed to fetch messages bulk:",j)}})()},[a,o]),E.useEffect(()=>{if(r&&r.type!=="raw"){const z=Oa(r.type,r.id);u(w=>{if(w[z]){const k={...w};return delete k[z],k}return w}),r.type==="channel"?We.markChannelRead(r.id).catch(w=>{console.error("Failed to mark channel as read on server:",w)}):r.type==="contact"&&We.markContactRead(r.id).catch(w=>{console.error("Failed to mark contact as read on server:",w)})}},[r]);const d=E.useCallback(z=>{u(w=>({...w,[z]:(w[z]||0)+1}))},[]),g=E.useCallback(()=>{u({}),We.markAllRead().catch(z=>{console.error("Failed to mark all as read on server:",z)})},[]),y=E.useCallback(z=>{if(z.type==="raw")return;const w=Oa(z.type,z.id);u(k=>{if(k[w]){const v={...k};return delete v[w],v}return k}),z.type==="channel"?We.markChannelRead(z.id).catch(k=>{console.error("Failed to mark channel as read on server:",k)}):z.type==="contact"&&We.markContactRead(z.id).catch(k=>{console.error("Failed to mark contact as read on server:",k)})},[]),f=E.useCallback(z=>{let w=null;if(z.type==="CHAN"&&z.conversation_key?w=Oa("channel",z.conversation_key):z.type==="PRIV"&&z.conversation_key&&(w=Oa("contact",z.conversation_key)),w){const k=z.received_at||Math.floor(Date.now()/1e3),v=ru(w,k);m(v)}},[]);return{unreadCounts:s,lastMessageTimes:l,incrementUnread:d,markAllRead:g,markConversationRead:y,trackNewMessage:f}}const Zs=200;function ou(a){return`${a.type}-${a.conversation_key}-${a.text}-${a.sender_timestamp}`}function fk(a){const[o,r]=E.useState([]),[s,u]=E.useState(!1),[l,m]=E.useState(!1),[c,h]=E.useState(!1),d=E.useRef(new Set),g=E.useCallback(async(w=!1)=>{if(!a||a.type==="raw"){r([]),h(!1);return}w&&(u(!0),r([]));try{const k=await We.getMessages({type:a.type==="channel"?"CHAN":"PRIV",conversation_key:a.id,limit:Zs});r(k),d.current.clear();for(const v of k)d.current.add(ou(v));h(k.length>=Zs)}catch(k){console.error("Failed to fetch messages:",k)}finally{w&&u(!1)}},[a]),y=E.useCallback(async()=>{if(!(!a||a.type==="raw"||l||!c)){m(!0);try{const w=await We.getMessages({type:a.type==="channel"?"CHAN":"PRIV",conversation_key:a.id,limit:Zs,offset:o.length});if(w.length>0){r(k=>[...k,...w]);for(const k of w)d.current.add(ou(k))}h(w.length>=Zs)}catch(w){console.error("Failed to fetch older messages:",w)}finally{m(!1)}}},[a,l,c,o.length]);E.useEffect(()=>{g(!0)},[g]);const f=E.useCallback(w=>{const k=ou(w);if(d.current.has(k))return console.debug("Duplicate message content ignored:",k.slice(0,50)),!1;if(d.current.add(k),d.current.size>1e3){const v=Array.from(d.current);d.current=new Set(v.slice(-500))}return r(v=>v.some(j=>j.id===w.id)?v:[...v,w]),!0},[]),z=E.useCallback((w,k)=>{r(v=>{const j=v.findIndex(q=>q.id===w);if(j>=0){const q=[...v];return q[j]={...v[j],acked:k},q}return v})},[]);return{messages:o,messagesLoading:s,loadingOlder:l,hasOlderMessages:c,setMessages:r,fetchMessages:g,fetchOlderMessages:y,addMessageIfNew:f,updateMessageAck:z}}/** * @license lucide-react v0.562.0 - ISC * * This source code is licensed under the ISC license. diff --git a/frontend/dist/index.html b/frontend/dist/index.html index 7a8ea93ad..b058c9b17 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -13,7 +13,7 @@ - + diff --git a/frontend/src/hooks/useConversationMessages.ts b/frontend/src/hooks/useConversationMessages.ts index 8e2f7aa5d..342e2c8b6 100644 --- a/frontend/src/hooks/useConversationMessages.ts +++ b/frontend/src/hooks/useConversationMessages.ts @@ -42,6 +42,8 @@ export function useConversationMessages( if (showLoading) { setMessagesLoading(true); + // Clear messages first so MessageList resets scroll state for new conversation + setMessages([]); } try { const data = await api.getMessages({