mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-02 19:43:10 +02:00
Make pagination requests abortable
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
|
||||
import{r as u,D as j,j as e,U as y,m as N}from"./index-BUPp0i0i.js";import{M as w,T as _}from"./leaflet-Nkpzzebg.js";import{C as k,P as C}from"./Popup-_4Won2BS.js";import{u as M}from"./hooks-fCOseuFw.js";const i={recent:"#06b6d4",today:"#2563eb",stale:"#f59e0b",old:"#64748b"},R="#0f172a",p="#f8fafc";function E(a){if(a==null)return i.old;const l=Date.now()/1e3-a,n=3600,t=86400;return l<n?i.recent:l<t?i.today:l<3*t?i.stale:i.old}function v({contacts:a,focusedContact:r}){const l=M(),[n,t]=u.useState(!1);return u.useEffect(()=>{if(r&&r.lat!=null&&r.lon!=null){l.setView([r.lat,r.lon],12),t(!0);return}if(n)return;const c=()=>{if(a.length===0){l.setView([20,0],2),t(!0);return}if(a.length===1){l.setView([a[0].lat,a[0].lon],10),t(!0);return}const d=a.map(m=>[m.lat,m.lon]);l.fitBounds(d,{padding:[50,50],maxZoom:12}),t(!0)};"geolocation"in navigator?navigator.geolocation.getCurrentPosition(d=>{l.setView([d.coords.latitude,d.coords.longitude],8),t(!0)},()=>{c()},{timeout:5e3,maximumAge:3e5}):c()},[l,a,n,r]),null}function V({contacts:a,focusedKey:r}){const[l]=u.useState(()=>Date.now()/1e3-604800),n=u.useMemo(()=>a.filter(s=>j(s.lat,s.lon)&&(s.public_key===r||s.last_seen!=null&&s.last_seen>l)),[a,r,l]),t=u.useMemo(()=>r&&n.find(s=>s.public_key===r)||null,[r,n]),c=t!=null&&(t.last_seen==null||t.last_seen<=l),d=u.useRef({}),m=u.useCallback((s,o)=>{d.current[s]=o},[]);return u.useEffect(()=>{if(t&&d.current[t.public_key]){const s=setTimeout(()=>{var o;(o=d.current[t.public_key])==null||o.openPopup()},100);return()=>clearTimeout(s)}},[t]),e.jsxs("div",{className:"flex flex-col h-full",children:[e.jsxs("div",{className:"px-4 py-2 bg-muted/50 text-xs text-muted-foreground flex items-center justify-between",children:[e.jsxs("span",{children:["Showing ",n.length," contact",n.length!==1?"s":""," heard in the last 7 days",c?" plus the focused contact":""]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full",style:{backgroundColor:i.recent},"aria-hidden":"true"})," ","<1h"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full",style:{backgroundColor:i.today},"aria-hidden":"true"})," ","<1d"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full",style:{backgroundColor:i.stale},"aria-hidden":"true"})," ","<3d"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full",style:{backgroundColor:i.old},"aria-hidden":"true"})," ","older"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full border-2",style:{borderColor:p,backgroundColor:i.today},"aria-hidden":"true"})," ","repeater"]})]})]}),e.jsx("div",{className:"flex-1 relative",style:{zIndex:0},role:"img","aria-label":"Map showing mesh node locations",children:e.jsxs(w,{center:[20,0],zoom:2,className:"h-full w-full",style:{background:"#1a1a2e"},children:[e.jsx(_,{attribution:'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',url:"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"}),e.jsx(v,{contacts:n,focusedContact:t}),n.map(s=>{const o=s.type===y,x=E(s.last_seen),f=s.name||s.public_key.slice(0,12),h=s.last_seen!=null?N(s.last_seen):"Never heard by this server",g=o?10:7;return e.jsx(u.Fragment,{children:e.jsx(k,{ref:b=>m(s.public_key,b),center:[s.lat,s.lon],radius:g,pathOptions:{color:o?p:R,fillColor:x,fillOpacity:.9,weight:o?3:2},children:e.jsx(C,{children:e.jsxs("div",{className:"text-sm",children:[e.jsxs("div",{className:"font-medium flex items-center gap-1",children:[o&&e.jsx("span",{title:"Repeater","aria-hidden":"true",children:"🛜"}),f]}),e.jsxs("div",{className:"text-xs text-gray-500 mt-1",children:["Last heard: ",h]}),e.jsxs("div",{className:"text-xs text-gray-400 mt-1 font-mono",children:[s.lat.toFixed(5),", ",s.lon.toFixed(5)]})]})})},s.public_key)},s.public_key)})]})})]})}export{V as MapView};
|
||||
//# sourceMappingURL=MapView-8l-XCyfo.js.map
|
||||
import{r as u,D as j,j as e,U as y,m as N}from"./index-C_33NHxn.js";import{M as w,T as _}from"./leaflet-DNHXPpW7.js";import{C as k,P as C}from"./Popup-BcLg2SsA.js";import{u as M}from"./hooks-BmAJqTPK.js";const i={recent:"#06b6d4",today:"#2563eb",stale:"#f59e0b",old:"#64748b"},R="#0f172a",p="#f8fafc";function E(a){if(a==null)return i.old;const l=Date.now()/1e3-a,n=3600,t=86400;return l<n?i.recent:l<t?i.today:l<3*t?i.stale:i.old}function v({contacts:a,focusedContact:r}){const l=M(),[n,t]=u.useState(!1);return u.useEffect(()=>{if(r&&r.lat!=null&&r.lon!=null){l.setView([r.lat,r.lon],12),t(!0);return}if(n)return;const c=()=>{if(a.length===0){l.setView([20,0],2),t(!0);return}if(a.length===1){l.setView([a[0].lat,a[0].lon],10),t(!0);return}const d=a.map(m=>[m.lat,m.lon]);l.fitBounds(d,{padding:[50,50],maxZoom:12}),t(!0)};"geolocation"in navigator?navigator.geolocation.getCurrentPosition(d=>{l.setView([d.coords.latitude,d.coords.longitude],8),t(!0)},()=>{c()},{timeout:5e3,maximumAge:3e5}):c()},[l,a,n,r]),null}function V({contacts:a,focusedKey:r}){const[l]=u.useState(()=>Date.now()/1e3-604800),n=u.useMemo(()=>a.filter(s=>j(s.lat,s.lon)&&(s.public_key===r||s.last_seen!=null&&s.last_seen>l)),[a,r,l]),t=u.useMemo(()=>r&&n.find(s=>s.public_key===r)||null,[r,n]),c=t!=null&&(t.last_seen==null||t.last_seen<=l),d=u.useRef({}),m=u.useCallback((s,o)=>{d.current[s]=o},[]);return u.useEffect(()=>{if(t&&d.current[t.public_key]){const s=setTimeout(()=>{var o;(o=d.current[t.public_key])==null||o.openPopup()},100);return()=>clearTimeout(s)}},[t]),e.jsxs("div",{className:"flex flex-col h-full",children:[e.jsxs("div",{className:"px-4 py-2 bg-muted/50 text-xs text-muted-foreground flex items-center justify-between",children:[e.jsxs("span",{children:["Showing ",n.length," contact",n.length!==1?"s":""," heard in the last 7 days",c?" plus the focused contact":""]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full",style:{backgroundColor:i.recent},"aria-hidden":"true"})," ","<1h"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full",style:{backgroundColor:i.today},"aria-hidden":"true"})," ","<1d"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full",style:{backgroundColor:i.stale},"aria-hidden":"true"})," ","<3d"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full",style:{backgroundColor:i.old},"aria-hidden":"true"})," ","older"]}),e.jsxs("span",{className:"flex items-center gap-1",children:[e.jsx("span",{className:"w-3 h-3 rounded-full border-2",style:{borderColor:p,backgroundColor:i.today},"aria-hidden":"true"})," ","repeater"]})]})]}),e.jsx("div",{className:"flex-1 relative",style:{zIndex:0},role:"img","aria-label":"Map showing mesh node locations",children:e.jsxs(w,{center:[20,0],zoom:2,className:"h-full w-full",style:{background:"#1a1a2e"},children:[e.jsx(_,{attribution:'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',url:"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"}),e.jsx(v,{contacts:n,focusedContact:t}),n.map(s=>{const o=s.type===y,x=E(s.last_seen),f=s.name||s.public_key.slice(0,12),h=s.last_seen!=null?N(s.last_seen):"Never heard by this server",g=o?10:7;return e.jsx(u.Fragment,{children:e.jsx(k,{ref:b=>m(s.public_key,b),center:[s.lat,s.lon],radius:g,pathOptions:{color:o?p:R,fillColor:x,fillOpacity:.9,weight:o?3:2},children:e.jsx(C,{children:e.jsxs("div",{className:"text-sm",children:[e.jsxs("div",{className:"font-medium flex items-center gap-1",children:[o&&e.jsx("span",{title:"Repeater","aria-hidden":"true",children:"🛜"}),f]}),e.jsxs("div",{className:"text-xs text-gray-500 mt-1",children:["Last heard: ",h]}),e.jsxs("div",{className:"text-xs text-gray-400 mt-1 font-mono",children:[s.lat.toFixed(5),", ",s.lon.toFixed(5)]})]})})},s.public_key)},s.public_key)})]})})]})}export{V as MapView};
|
||||
//# sourceMappingURL=MapView-B_EeV5Ur.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
|
||||
import{j as l}from"./index-BUPp0i0i.js";import{d as m,l as u,a as f,e as d,M as x,T as y}from"./leaflet-Nkpzzebg.js";import{C as p,P as c}from"./Popup-_4Won2BS.js";const g=m(function({positions:n,...t},o){const s=new u.Polyline(n,t);return f(s,d(o,{overlayContainer:s}))},function(n,t,o){t.positions!==o.positions&&n.setLatLngs(t.positions)});function C({neighbors:i,radioLat:n,radioLon:t,radioName:o}){const s=i.filter(e=>e.lat!=null&&e.lon!=null),r=n!=null&&t!=null&&!(n===0&&t===0);if(s.length===0&&!r)return null;const h=r?[n,t]:[s[0].lat,s[0].lon];return l.jsx("div",{className:"min-h-48 flex-1 rounded border border-border overflow-hidden",role:"img","aria-label":"Map showing repeater neighbor locations",children:l.jsxs(x,{center:h,zoom:10,className:"h-full w-full",style:{background:"#1a1a2e"},children:[l.jsx(y,{attribution:'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',url:"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"}),r&&s.map((e,a)=>l.jsx(g,{positions:[[n,t],[e.lat,e.lon]],pathOptions:{color:"#3b82f6",weight:1.5,opacity:.5,dashArray:"6 4"}},`line-${a}`)),r&&l.jsx(p,{center:[n,t],radius:8,pathOptions:{color:"#1d4ed8",fillColor:"#3b82f6",fillOpacity:1,weight:2},children:l.jsx(c,{children:l.jsx("span",{className:"text-sm font-medium",children:o||"Our Radio"})})}),s.map((e,a)=>l.jsx(p,{center:[e.lat,e.lon],radius:6,pathOptions:{color:"#000",fillColor:e.snr>=6?"#22c55e":e.snr>=0?"#eab308":"#ef4444",fillOpacity:.8,weight:1},children:l.jsx(c,{children:l.jsx("span",{className:"text-sm",children:e.name||e.pubkey_prefix})})},a))]})})}export{C as NeighborsMiniMap};
|
||||
//# sourceMappingURL=NeighborsMiniMap-CI9vDONH.js.map
|
||||
import{j as l}from"./index-C_33NHxn.js";import{d as m,l as u,a as f,e as d,M as x,T as y}from"./leaflet-DNHXPpW7.js";import{C as p,P as c}from"./Popup-BcLg2SsA.js";const g=m(function({positions:n,...t},o){const s=new u.Polyline(n,t);return f(s,d(o,{overlayContainer:s}))},function(n,t,o){t.positions!==o.positions&&n.setLatLngs(t.positions)});function C({neighbors:i,radioLat:n,radioLon:t,radioName:o}){const s=i.filter(e=>e.lat!=null&&e.lon!=null),r=n!=null&&t!=null&&!(n===0&&t===0);if(s.length===0&&!r)return null;const h=r?[n,t]:[s[0].lat,s[0].lon];return l.jsx("div",{className:"min-h-48 flex-1 rounded border border-border overflow-hidden",role:"img","aria-label":"Map showing repeater neighbor locations",children:l.jsxs(x,{center:h,zoom:10,className:"h-full w-full",style:{background:"#1a1a2e"},children:[l.jsx(y,{attribution:'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',url:"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"}),r&&s.map((e,a)=>l.jsx(g,{positions:[[n,t],[e.lat,e.lon]],pathOptions:{color:"#3b82f6",weight:1.5,opacity:.5,dashArray:"6 4"}},`line-${a}`)),r&&l.jsx(p,{center:[n,t],radius:8,pathOptions:{color:"#1d4ed8",fillColor:"#3b82f6",fillOpacity:1,weight:2},children:l.jsx(c,{children:l.jsx("span",{className:"text-sm font-medium",children:o||"Our Radio"})})}),s.map((e,a)=>l.jsx(p,{center:[e.lat,e.lon],radius:6,pathOptions:{color:"#000",fillColor:e.snr>=6?"#22c55e":e.snr>=0?"#eab308":"#ef4444",fillOpacity:.8,weight:1},children:l.jsx(c,{children:l.jsx("span",{className:"text-sm",children:e.name||e.pubkey_prefix})})},a))]})})}export{C as NeighborsMiniMap};
|
||||
//# sourceMappingURL=NeighborsMiniMap-CsH85D0E.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
import{r as x,D as l,j as i}from"./index-BUPp0i0i.js";import{c as O,l as b,a as y,e as w,b as C,M as L,T as M,L as R}from"./leaflet-Nkpzzebg.js";import{u as T}from"./hooks-fCOseuFw.js";const h=O(function({position:n,...t},o){const r=new b.Marker(n,t);return y(r,w(o,{overlayContainer:r}))},function(n,t,o){t.position!==o.position&&n.setLatLng(t.position),t.icon!=null&&t.icon!==o.icon&&n.setIcon(t.icon),t.zIndexOffset!=null&&t.zIndexOffset!==o.zIndexOffset&&n.setZIndexOffset(t.zIndexOffset),t.opacity!=null&&t.opacity!==o.opacity&&n.setOpacity(t.opacity),n.dragging!=null&&t.draggable!==o.draggable&&(t.draggable===!0?n.dragging.enable():n.dragging.disable())}),p=C(function(n,t){const o=new b.Tooltip(n,t.overlayContainer);return y(o,t)},function(n,t,{position:o},r){x.useEffect(function(){const s=t.overlayContainer;if(s==null)return;const{instance:u}=n,f=a=>{a.tooltip===u&&(o!=null&&u.setLatLng(o),u.update(),r(!0))},c=a=>{a.tooltip===u&&r(!1)};return s.on({tooltipopen:f,tooltipclose:c}),s.bindTooltip(u),function(){s.off({tooltipopen:f,tooltipclose:c}),s._map!=null&&s.unbindTooltip()}},[n,t,r,o])}),m=["#f97316","#eab308","#22c55e","#06b6d4","#ec4899","#f43f5e","#a855f7","#64748b"],E="#3b82f6",S="#8b5cf6";function g(e,n){return R.divIcon({className:"",iconSize:[24,24],iconAnchor:[12,12],html:`<div style="
|
||||
import{r as x,D as l,j as i}from"./index-C_33NHxn.js";import{c as O,l as b,a as y,e as w,b as C,M as L,T as M,L as R}from"./leaflet-DNHXPpW7.js";import{u as T}from"./hooks-BmAJqTPK.js";const h=O(function({position:n,...t},o){const r=new b.Marker(n,t);return y(r,w(o,{overlayContainer:r}))},function(n,t,o){t.position!==o.position&&n.setLatLng(t.position),t.icon!=null&&t.icon!==o.icon&&n.setIcon(t.icon),t.zIndexOffset!=null&&t.zIndexOffset!==o.zIndexOffset&&n.setZIndexOffset(t.zIndexOffset),t.opacity!=null&&t.opacity!==o.opacity&&n.setOpacity(t.opacity),n.dragging!=null&&t.draggable!==o.draggable&&(t.draggable===!0?n.dragging.enable():n.dragging.disable())}),p=C(function(n,t){const o=new b.Tooltip(n,t.overlayContainer);return y(o,t)},function(n,t,{position:o},r){x.useEffect(function(){const s=t.overlayContainer;if(s==null)return;const{instance:u}=n,f=a=>{a.tooltip===u&&(o!=null&&u.setLatLng(o),u.update(),r(!0))},c=a=>{a.tooltip===u&&r(!1)};return s.on({tooltipopen:f,tooltipclose:c}),s.bindTooltip(u),function(){s.off({tooltipopen:f,tooltipclose:c}),s._map!=null&&s.unbindTooltip()}},[n,t,r,o])}),m=["#f97316","#eab308","#22c55e","#06b6d4","#ec4899","#f43f5e","#a855f7","#64748b"],E="#3b82f6",S="#8b5cf6";function g(e,n){return R.divIcon({className:"",iconSize:[24,24],iconAnchor:[12,12],html:`<div style="
|
||||
width:24px;height:24px;border-radius:50%;
|
||||
background:${n};color:#fff;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
@@ -6,4 +6,4 @@ import{r as x,D as l,j as i}from"./index-BUPp0i0i.js";import{c as O,l as b,a as
|
||||
border:2px solid rgba(255,255,255,0.8);
|
||||
box-shadow:0 1px 4px rgba(0,0,0,0.4);
|
||||
">${e}</div>`})}function z(e){return m[e%m.length]}function N(e){const n=[];l(e.sender.lat,e.sender.lon)&&n.push([e.sender.lat,e.sender.lon]);for(const t of e.hops)for(const o of t.matches)l(o.lat,o.lon)&&n.push([o.lat,o.lon]);return l(e.receiver.lat,e.receiver.lon)&&n.push([e.receiver.lat,e.receiver.lon]),n}function I({points:e}){const n=T(),t=x.useRef(!1);return x.useEffect(()=>{t.current||e.length===0||(t.current=!0,e.length===1?n.setView(e[0],12):n.fitBounds(e,{padding:[30,30],maxZoom:14}))},[n,e]),null}function k({resolved:e,senderInfo:n}){const t=N(e),o=t.length>0;let r=2,d=0;l(e.sender.lat,e.sender.lon)&&d++,l(e.receiver.lat,e.receiver.lon)&&d++;for(const f of e.hops)f.matches.length===0?r++:(r+=f.matches.length,d+=f.matches.filter(c=>l(c.lat,c.lon)).length);const s=o&&d<r;if(!o)return i.jsx("div",{className:"h-14 rounded border border-border bg-muted/30 flex items-center justify-center text-sm text-muted-foreground",children:"No nodes in this route have GPS coordinates"});const u=t[0];return i.jsxs("div",{children:[i.jsx("div",{className:"rounded border border-border overflow-hidden",role:"img","aria-label":"Map showing message route between nodes",style:{height:220},children:i.jsxs(L,{center:u,zoom:10,className:"h-full w-full",style:{background:"#1a1a2e"},children:[i.jsx(M,{attribution:'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',url:"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"}),i.jsx(I,{points:t}),l(e.sender.lat,e.sender.lon)&&i.jsx(h,{position:[e.sender.lat,e.sender.lon],icon:g("S",E),children:i.jsx(p,{direction:"top",offset:[0,-14],children:n.name||"Sender"})}),e.hops.map((f,c)=>f.matches.filter(a=>l(a.lat,a.lon)).map((a,j)=>i.jsx(h,{position:[a.lat,a.lon],icon:g(String(c+1),z(c)),children:i.jsx(p,{direction:"top",offset:[0,-14],children:a.name||a.public_key.slice(0,12)})},`hop-${c}-${j}`))),l(e.receiver.lat,e.receiver.lon)&&i.jsx(h,{position:[e.receiver.lat,e.receiver.lon],icon:g("R",S),children:i.jsx(p,{direction:"top",offset:[0,-14],children:e.receiver.name||"Receiver"})})]})}),s&&i.jsx("p",{className:"text-xs text-muted-foreground mt-1",children:"Some nodes in this route have no GPS and are not shown"})]})}export{k as PathRouteMap};
|
||||
//# sourceMappingURL=PathRouteMap--4_fo75Z.js.map
|
||||
//# sourceMappingURL=PathRouteMap-DbzDNGyG.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
|
||||
import{d,l as f,a as s,e as C,b as m}from"./leaflet-Nkpzzebg.js";import{r as P}from"./index-BUPp0i0i.js";function v(o,n,e){n.center!==e.center&&o.setLatLng(n.center),n.radius!=null&&n.radius!==e.radius&&o.setRadius(n.radius)}const b=d(function({center:n,children:e,...r},p){const t=new f.CircleMarker(n,r);return s(t,C(p,{overlayContainer:t}))},v),k=m(function(n,e){const r=new f.Popup(n,e.overlayContainer);return s(r,e)},function(n,e,{position:r},p){P.useEffect(function(){const{instance:a}=n;function i(u){u.popup===a&&(a.update(),p(!0))}function c(u){u.popup===a&&p(!1)}return e.map.on({popupopen:i,popupclose:c}),e.overlayContainer==null?(r!=null&&a.setLatLng(r),a.openOn(e.map)):e.overlayContainer.bindPopup(a),function(){var l;e.map.off({popupopen:i,popupclose:c}),(l=e.overlayContainer)==null||l.unbindPopup(),e.map.removeLayer(a)}},[n,e,p,r])});export{b as C,k as P};
|
||||
//# sourceMappingURL=Popup-_4Won2BS.js.map
|
||||
import{d,l as f,a as s,e as C,b as m}from"./leaflet-DNHXPpW7.js";import{r as P}from"./index-C_33NHxn.js";function v(o,n,e){n.center!==e.center&&o.setLatLng(n.center),n.radius!=null&&n.radius!==e.radius&&o.setRadius(n.radius)}const b=d(function({center:n,children:e,...r},p){const t=new f.CircleMarker(n,r);return s(t,C(p,{overlayContainer:t}))},v),k=m(function(n,e){const r=new f.Popup(n,e.overlayContainer);return s(r,e)},function(n,e,{position:r},p){P.useEffect(function(){const{instance:a}=n;function i(u){u.popup===a&&(a.update(),p(!0))}function c(u){u.popup===a&&p(!1)}return e.map.on({popupopen:i,popupclose:c}),e.overlayContainer==null?(r!=null&&a.setLatLng(r),a.openOn(e.map)):e.overlayContainer.bindPopup(a),function(){var l;e.map.off({popupopen:i,popupclose:c}),(l=e.overlayContainer)==null||l.unbindPopup(),e.map.removeLayer(a)}},[n,e,p,r])});export{b as C,k as P};
|
||||
//# sourceMappingURL=Popup-BcLg2SsA.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"Popup-_4Won2BS.js","sources":["../../node_modules/@react-leaflet/core/lib/circle.js","../../node_modules/react-leaflet/lib/CircleMarker.js","../../node_modules/react-leaflet/lib/Popup.js"],"sourcesContent":["export function updateCircle(layer, props, prevProps) {\n if (props.center !== prevProps.center) {\n layer.setLatLng(props.center);\n }\n if (props.radius != null && props.radius !== prevProps.radius) {\n layer.setRadius(props.radius);\n }\n}\n","import { createElementObject, createPathComponent, extendContext, updateCircle } from '@react-leaflet/core';\nimport { CircleMarker as LeafletCircleMarker } from 'leaflet';\nexport const CircleMarker = createPathComponent(function createCircleMarker({ center , children: _c , ...options }, ctx) {\n const marker = new LeafletCircleMarker(center, options);\n return createElementObject(marker, extendContext(ctx, {\n overlayContainer: marker\n }));\n}, updateCircle);\n","import { createElementObject, createOverlayComponent } from '@react-leaflet/core';\nimport { Popup as LeafletPopup } from 'leaflet';\nimport { useEffect } from 'react';\nexport const Popup = createOverlayComponent(function createPopup(props, context) {\n const popup = new LeafletPopup(props, context.overlayContainer);\n return createElementObject(popup, context);\n}, function usePopupLifecycle(element, context, { position }, setOpen) {\n useEffect(function addPopup() {\n const { instance } = element;\n function onPopupOpen(event) {\n if (event.popup === instance) {\n instance.update();\n setOpen(true);\n }\n }\n function onPopupClose(event) {\n if (event.popup === instance) {\n setOpen(false);\n }\n }\n context.map.on({\n popupopen: onPopupOpen,\n popupclose: onPopupClose\n });\n if (context.overlayContainer == null) {\n // Attach to a Map\n if (position != null) {\n instance.setLatLng(position);\n }\n instance.openOn(context.map);\n } else {\n // Attach to container component\n context.overlayContainer.bindPopup(instance);\n }\n return function removePopup() {\n context.map.off({\n popupopen: onPopupOpen,\n popupclose: onPopupClose\n });\n context.overlayContainer?.unbindPopup();\n context.map.removeLayer(instance);\n };\n }, [\n element,\n context,\n setOpen,\n position\n ]);\n});\n"],"names":["updateCircle","layer","props","prevProps","CircleMarker","createPathComponent","center","_c","options","ctx","marker","LeafletCircleMarker","createElementObject","extendContext","Popup","createOverlayComponent","context","popup","LeafletPopup","element","position","setOpen","useEffect","instance","onPopupOpen","event","onPopupClose","_a"],"mappings":"yGAAO,SAASA,EAAaC,EAAOC,EAAOC,EAAW,CAC9CD,EAAM,SAAWC,EAAU,QAC3BF,EAAM,UAAUC,EAAM,MAAM,EAE5BA,EAAM,QAAU,MAAQA,EAAM,SAAWC,EAAU,QACnDF,EAAM,UAAUC,EAAM,MAAM,CAEpC,CCLY,MAACE,EAAeC,EAAoB,SAA4B,CAAE,OAAAC,EAAS,SAAUC,EAAK,GAAGC,CAAO,EAAIC,EAAK,CACrH,MAAMC,EAAS,IAAIC,eAAoBL,EAAQE,CAAO,EACtD,OAAOI,EAAoBF,EAAQG,EAAcJ,EAAK,CAClD,iBAAkBC,CAC1B,CAAK,CAAC,CACN,EAAGV,CAAY,ECJFc,EAAQC,EAAuB,SAAqBb,EAAOc,EAAS,CAC7E,MAAMC,EAAQ,IAAIC,EAAAA,MAAahB,EAAOc,EAAQ,gBAAgB,EAC9D,OAAOJ,EAAoBK,EAAOD,CAAO,CAC7C,EAAG,SAA2BG,EAASH,EAAS,CAAE,SAAAI,CAAQ,EAAKC,EAAS,CACpEC,EAAAA,UAAU,UAAoB,CAC1B,KAAM,CAAE,SAAAC,CAAQ,EAAMJ,EACtB,SAASK,EAAYC,EAAO,CACpBA,EAAM,QAAUF,IAChBA,EAAS,OAAM,EACfF,EAAQ,EAAI,EAEpB,CACA,SAASK,EAAaD,EAAO,CACrBA,EAAM,QAAUF,GAChBF,EAAQ,EAAK,CAErB,CACA,OAAAL,EAAQ,IAAI,GAAG,CACX,UAAWQ,EACX,WAAYE,CACxB,CAAS,EACGV,EAAQ,kBAAoB,MAExBI,GAAY,MACZG,EAAS,UAAUH,CAAQ,EAE/BG,EAAS,OAAOP,EAAQ,GAAG,GAG3BA,EAAQ,iBAAiB,UAAUO,CAAQ,EAExC,UAAuB,OAC1BP,EAAQ,IAAI,IAAI,CACZ,UAAWQ,EACX,WAAYE,CAC5B,CAAa,GACDC,EAAAX,EAAQ,mBAAR,MAAAW,EAA0B,cAC1BX,EAAQ,IAAI,YAAYO,CAAQ,CACpC,CACJ,EAAG,CACCJ,EACAH,EACAK,EACAD,CACR,CAAK,CACL,CAAC","x_google_ignoreList":[0,1,2]}
|
||||
{"version":3,"file":"Popup-BcLg2SsA.js","sources":["../../node_modules/@react-leaflet/core/lib/circle.js","../../node_modules/react-leaflet/lib/CircleMarker.js","../../node_modules/react-leaflet/lib/Popup.js"],"sourcesContent":["export function updateCircle(layer, props, prevProps) {\n if (props.center !== prevProps.center) {\n layer.setLatLng(props.center);\n }\n if (props.radius != null && props.radius !== prevProps.radius) {\n layer.setRadius(props.radius);\n }\n}\n","import { createElementObject, createPathComponent, extendContext, updateCircle } from '@react-leaflet/core';\nimport { CircleMarker as LeafletCircleMarker } from 'leaflet';\nexport const CircleMarker = createPathComponent(function createCircleMarker({ center , children: _c , ...options }, ctx) {\n const marker = new LeafletCircleMarker(center, options);\n return createElementObject(marker, extendContext(ctx, {\n overlayContainer: marker\n }));\n}, updateCircle);\n","import { createElementObject, createOverlayComponent } from '@react-leaflet/core';\nimport { Popup as LeafletPopup } from 'leaflet';\nimport { useEffect } from 'react';\nexport const Popup = createOverlayComponent(function createPopup(props, context) {\n const popup = new LeafletPopup(props, context.overlayContainer);\n return createElementObject(popup, context);\n}, function usePopupLifecycle(element, context, { position }, setOpen) {\n useEffect(function addPopup() {\n const { instance } = element;\n function onPopupOpen(event) {\n if (event.popup === instance) {\n instance.update();\n setOpen(true);\n }\n }\n function onPopupClose(event) {\n if (event.popup === instance) {\n setOpen(false);\n }\n }\n context.map.on({\n popupopen: onPopupOpen,\n popupclose: onPopupClose\n });\n if (context.overlayContainer == null) {\n // Attach to a Map\n if (position != null) {\n instance.setLatLng(position);\n }\n instance.openOn(context.map);\n } else {\n // Attach to container component\n context.overlayContainer.bindPopup(instance);\n }\n return function removePopup() {\n context.map.off({\n popupopen: onPopupOpen,\n popupclose: onPopupClose\n });\n context.overlayContainer?.unbindPopup();\n context.map.removeLayer(instance);\n };\n }, [\n element,\n context,\n setOpen,\n position\n ]);\n});\n"],"names":["updateCircle","layer","props","prevProps","CircleMarker","createPathComponent","center","_c","options","ctx","marker","LeafletCircleMarker","createElementObject","extendContext","Popup","createOverlayComponent","context","popup","LeafletPopup","element","position","setOpen","useEffect","instance","onPopupOpen","event","onPopupClose","_a"],"mappings":"yGAAO,SAASA,EAAaC,EAAOC,EAAOC,EAAW,CAC9CD,EAAM,SAAWC,EAAU,QAC3BF,EAAM,UAAUC,EAAM,MAAM,EAE5BA,EAAM,QAAU,MAAQA,EAAM,SAAWC,EAAU,QACnDF,EAAM,UAAUC,EAAM,MAAM,CAEpC,CCLY,MAACE,EAAeC,EAAoB,SAA4B,CAAE,OAAAC,EAAS,SAAUC,EAAK,GAAGC,CAAO,EAAIC,EAAK,CACrH,MAAMC,EAAS,IAAIC,eAAoBL,EAAQE,CAAO,EACtD,OAAOI,EAAoBF,EAAQG,EAAcJ,EAAK,CAClD,iBAAkBC,CAC1B,CAAK,CAAC,CACN,EAAGV,CAAY,ECJFc,EAAQC,EAAuB,SAAqBb,EAAOc,EAAS,CAC7E,MAAMC,EAAQ,IAAIC,EAAAA,MAAahB,EAAOc,EAAQ,gBAAgB,EAC9D,OAAOJ,EAAoBK,EAAOD,CAAO,CAC7C,EAAG,SAA2BG,EAASH,EAAS,CAAE,SAAAI,CAAQ,EAAKC,EAAS,CACpEC,EAAAA,UAAU,UAAoB,CAC1B,KAAM,CAAE,SAAAC,CAAQ,EAAMJ,EACtB,SAASK,EAAYC,EAAO,CACpBA,EAAM,QAAUF,IAChBA,EAAS,OAAM,EACfF,EAAQ,EAAI,EAEpB,CACA,SAASK,EAAaD,EAAO,CACrBA,EAAM,QAAUF,GAChBF,EAAQ,EAAK,CAErB,CACA,OAAAL,EAAQ,IAAI,GAAG,CACX,UAAWQ,EACX,WAAYE,CACxB,CAAS,EACGV,EAAQ,kBAAoB,MAExBI,GAAY,MACZG,EAAS,UAAUH,CAAQ,EAE/BG,EAAS,OAAOP,EAAQ,GAAG,GAG3BA,EAAQ,iBAAiB,UAAUO,CAAQ,EAExC,UAAuB,OAC1BP,EAAQ,IAAI,IAAI,CACZ,UAAWQ,EACX,WAAYE,CAC5B,CAAa,GACDC,EAAAX,EAAQ,mBAAR,MAAAW,EAA0B,cAC1BX,EAAQ,IAAI,YAAYO,CAAQ,CACpC,CACJ,EAAG,CACCJ,EACAH,EACAK,EACAD,CACR,CAAK,CACL,CAAC","x_google_ignoreList":[0,1,2]}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
frontend/prebuilt/assets/hooks-BmAJqTPK.js
Normal file
2
frontend/prebuilt/assets/hooks-BmAJqTPK.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import"./index-C_33NHxn.js";import{u as t}from"./leaflet-DNHXPpW7.js";function r(){return t().map}export{r as u};
|
||||
//# sourceMappingURL=hooks-BmAJqTPK.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"hooks-fCOseuFw.js","sources":["../../node_modules/react-leaflet/lib/hooks.js"],"sourcesContent":["import { useLeafletContext } from '@react-leaflet/core';\nimport { useEffect } from 'react';\nexport function useMap() {\n return useLeafletContext().map;\n}\nexport function useMapEvent(type, handler) {\n const map = useMap();\n useEffect(function addMapEventHandler() {\n // @ts-ignore event type\n map.on(type, handler);\n return function removeMapEventHandler() {\n // @ts-ignore event type\n map.off(type, handler);\n };\n }, [\n map,\n type,\n handler\n ]);\n return map;\n}\nexport function useMapEvents(handlers) {\n const map = useMap();\n useEffect(function addMapEventHandlers() {\n map.on(handlers);\n return function removeMapEventHandlers() {\n map.off(handlers);\n };\n }, [\n map,\n handlers\n ]);\n return map;\n}\n"],"names":["useMap","useLeafletContext"],"mappings":"sEAEO,SAASA,GAAS,CACrB,OAAOC,EAAiB,EAAG,GAC/B","x_google_ignoreList":[0]}
|
||||
{"version":3,"file":"hooks-BmAJqTPK.js","sources":["../../node_modules/react-leaflet/lib/hooks.js"],"sourcesContent":["import { useLeafletContext } from '@react-leaflet/core';\nimport { useEffect } from 'react';\nexport function useMap() {\n return useLeafletContext().map;\n}\nexport function useMapEvent(type, handler) {\n const map = useMap();\n useEffect(function addMapEventHandler() {\n // @ts-ignore event type\n map.on(type, handler);\n return function removeMapEventHandler() {\n // @ts-ignore event type\n map.off(type, handler);\n };\n }, [\n map,\n type,\n handler\n ]);\n return map;\n}\nexport function useMapEvents(handlers) {\n const map = useMap();\n useEffect(function addMapEventHandlers() {\n map.on(handlers);\n return function removeMapEventHandlers() {\n map.off(handlers);\n };\n }, [\n map,\n handlers\n ]);\n return map;\n}\n"],"names":["useMap","useLeafletContext"],"mappings":"sEAEO,SAASA,GAAS,CACrB,OAAOC,EAAiB,EAAG,GAC/B","x_google_ignoreList":[0]}
|
||||
@@ -1,2 +0,0 @@
|
||||
import"./index-BUPp0i0i.js";import{u as t}from"./leaflet-Nkpzzebg.js";function r(){return t().map}export{r as u};
|
||||
//# sourceMappingURL=hooks-fCOseuFw.js.map
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
261
frontend/prebuilt/assets/index-C_33NHxn.js
Normal file
261
frontend/prebuilt/assets/index-C_33NHxn.js
Normal file
File diff suppressed because one or more lines are too long
1
frontend/prebuilt/assets/index-C_33NHxn.js.map
Normal file
1
frontend/prebuilt/assets/index-C_33NHxn.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
|
||||
import{r as s,j as l,Q as f,l as u}from"./index-BUPp0i0i.js";var N=["a","button","div","form","h2","h3","img","input","label","li","nav","ol","p","select","span","svg","ul"],h=N.reduce((a,r)=>{const t=f(`Primitive.${r}`),o=s.forwardRef((i,e)=>{const{asChild:p,...n}=i,m=p?t:r;return typeof window<"u"&&(window[Symbol.for("radix-ui")]=!0),l.jsx(m,{...n,ref:e})});return o.displayName=`Primitive.${r}`,{...a,[r]:o}},{}),x="Separator",c="horizontal",w=["horizontal","vertical"],d=s.forwardRef((a,r)=>{const{decorative:t,orientation:o=c,...i}=a,e=S(o)?o:c,n=t?{role:"none"}:{"aria-orientation":e==="vertical"?e:void 0,role:"separator"};return l.jsx(h.div,{"data-orientation":e,...n,...i,ref:r})});d.displayName=x;function S(a){return w.includes(a)}var v=d;const O=s.forwardRef(({className:a,orientation:r="horizontal",decorative:t=!0,...o},i)=>l.jsx(v,{ref:i,decorative:t,orientation:r,className:u("shrink-0 bg-border",r==="horizontal"?"h-[1px] w-full":"h-full w-[1px]",a),...o}));O.displayName=v.displayName;export{O as S};
|
||||
//# sourceMappingURL=separator-C7ZN2sFh.js.map
|
||||
import{r as s,j as l,Q as f,l as u}from"./index-C_33NHxn.js";var N=["a","button","div","form","h2","h3","img","input","label","li","nav","ol","p","select","span","svg","ul"],h=N.reduce((a,r)=>{const t=f(`Primitive.${r}`),o=s.forwardRef((i,e)=>{const{asChild:p,...n}=i,m=p?t:r;return typeof window<"u"&&(window[Symbol.for("radix-ui")]=!0),l.jsx(m,{...n,ref:e})});return o.displayName=`Primitive.${r}`,{...a,[r]:o}},{}),x="Separator",c="horizontal",w=["horizontal","vertical"],d=s.forwardRef((a,r)=>{const{decorative:t,orientation:o=c,...i}=a,e=S(o)?o:c,n=t?{role:"none"}:{"aria-orientation":e==="vertical"?e:void 0,role:"separator"};return l.jsx(h.div,{"data-orientation":e,...n,...i,ref:r})});d.displayName=x;function S(a){return w.includes(a)}var v=d;const O=s.forwardRef(({className:a,orientation:r="horizontal",decorative:t=!0,...o},i)=>l.jsx(v,{ref:i,decorative:t,orientation:r,className:u("shrink-0 bg-border",r==="horizontal"?"h-[1px] w-full":"h-full w-[1px]",a),...o}));O.displayName=v.displayName;export{O as S};
|
||||
//# sourceMappingURL=separator-BtL4plOj.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -50,7 +50,7 @@
|
||||
undecryptedCount: fetchJsonOrThrow('/api/packets/undecrypted/count'),
|
||||
};
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-BUPp0i0i.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-C_33NHxn.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Cx5ENk0V.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -161,6 +161,8 @@ export function useConversationMessages(
|
||||
const [loadingNewer, setLoadingNewer] = useState(false);
|
||||
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const olderAbortControllerRef = useRef<AbortController | null>(null);
|
||||
const newerAbortControllerRef = useRef<AbortController | null>(null);
|
||||
const fetchingConversationIdRef = useRef<string | null>(null);
|
||||
const latestReconcileRequestIdRef = useRef(0);
|
||||
const messagesRef = useRef<Message[]>([]);
|
||||
@@ -306,14 +308,19 @@ export function useConversationMessages(
|
||||
|
||||
loadingOlderRef.current = true;
|
||||
setLoadingOlder(true);
|
||||
const controller = new AbortController();
|
||||
olderAbortControllerRef.current = controller;
|
||||
try {
|
||||
const data = await api.getMessages({
|
||||
type: activeConversation.type === 'channel' ? 'CHAN' : 'PRIV',
|
||||
conversation_key: conversationId,
|
||||
limit: MESSAGE_PAGE_SIZE,
|
||||
before: oldestMessage.received_at,
|
||||
before_id: oldestMessage.id,
|
||||
});
|
||||
const data = await api.getMessages(
|
||||
{
|
||||
type: activeConversation.type === 'channel' ? 'CHAN' : 'PRIV',
|
||||
conversation_key: conversationId,
|
||||
limit: MESSAGE_PAGE_SIZE,
|
||||
before: oldestMessage.received_at,
|
||||
before_id: oldestMessage.id,
|
||||
},
|
||||
controller.signal
|
||||
);
|
||||
|
||||
if (fetchingConversationIdRef.current !== conversationId) return;
|
||||
|
||||
@@ -335,11 +342,17 @@ export function useConversationMessages(
|
||||
}
|
||||
setHasOlderMessages(dataWithPendingAck.length >= MESSAGE_PAGE_SIZE);
|
||||
} catch (err) {
|
||||
if (isAbortError(err)) {
|
||||
return;
|
||||
}
|
||||
console.error('Failed to fetch older messages:', err);
|
||||
toast.error('Failed to load older messages', {
|
||||
description: err instanceof Error ? err.message : 'Check your connection',
|
||||
});
|
||||
} finally {
|
||||
if (olderAbortControllerRef.current === controller) {
|
||||
olderAbortControllerRef.current = null;
|
||||
}
|
||||
loadingOlderRef.current = false;
|
||||
setLoadingOlder(false);
|
||||
}
|
||||
@@ -361,14 +374,19 @@ export function useConversationMessages(
|
||||
if (!newestMessage) return;
|
||||
|
||||
setLoadingNewer(true);
|
||||
const controller = new AbortController();
|
||||
newerAbortControllerRef.current = controller;
|
||||
try {
|
||||
const data = await api.getMessages({
|
||||
type: activeConversation.type === 'channel' ? 'CHAN' : 'PRIV',
|
||||
conversation_key: conversationId,
|
||||
limit: MESSAGE_PAGE_SIZE,
|
||||
after: newestMessage.received_at,
|
||||
after_id: newestMessage.id,
|
||||
});
|
||||
const data = await api.getMessages(
|
||||
{
|
||||
type: activeConversation.type === 'channel' ? 'CHAN' : 'PRIV',
|
||||
conversation_key: conversationId,
|
||||
limit: MESSAGE_PAGE_SIZE,
|
||||
after: newestMessage.received_at,
|
||||
after_id: newestMessage.id,
|
||||
},
|
||||
controller.signal
|
||||
);
|
||||
|
||||
if (fetchingConversationIdRef.current !== conversationId) return;
|
||||
|
||||
@@ -385,11 +403,17 @@ export function useConversationMessages(
|
||||
}
|
||||
setHasNewerMessages(dataWithPendingAck.length >= MESSAGE_PAGE_SIZE);
|
||||
} catch (err) {
|
||||
if (isAbortError(err)) {
|
||||
return;
|
||||
}
|
||||
console.error('Failed to fetch newer messages:', err);
|
||||
toast.error('Failed to load newer messages', {
|
||||
description: err instanceof Error ? err.message : 'Check your connection',
|
||||
});
|
||||
} finally {
|
||||
if (newerAbortControllerRef.current === controller) {
|
||||
newerAbortControllerRef.current = null;
|
||||
}
|
||||
setLoadingNewer(false);
|
||||
}
|
||||
}, [activeConversation, applyPendingAck, hasNewerMessages, loadingNewer, messages]);
|
||||
@@ -420,6 +444,14 @@ export function useConversationMessages(
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
if (olderAbortControllerRef.current) {
|
||||
olderAbortControllerRef.current.abort();
|
||||
olderAbortControllerRef.current = null;
|
||||
}
|
||||
if (newerAbortControllerRef.current) {
|
||||
newerAbortControllerRef.current.abort();
|
||||
newerAbortControllerRef.current = null;
|
||||
}
|
||||
|
||||
const prevId = prevConversationIdRef.current;
|
||||
const newId = activeConversation?.id ?? null;
|
||||
|
||||
@@ -2,20 +2,28 @@ import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||
|
||||
import * as messageCache from '../messageCache';
|
||||
import { api } from '../api';
|
||||
import { useConversationMessages } from '../hooks/useConversationMessages';
|
||||
import type { Conversation, Message } from '../types';
|
||||
|
||||
const mockGetMessages = vi.fn<(...args: unknown[]) => Promise<Message[]>>();
|
||||
const mockGetMessages = vi.fn<typeof api.getMessages>();
|
||||
const mockGetMessagesAround = vi.fn();
|
||||
|
||||
vi.mock('../api', () => ({
|
||||
api: {
|
||||
getMessages: (...args: unknown[]) => mockGetMessages(...args),
|
||||
getMessages: (...args: Parameters<typeof api.getMessages>) => mockGetMessages(...args),
|
||||
getMessagesAround: (...args: unknown[]) => mockGetMessagesAround(...args),
|
||||
},
|
||||
isAbortError: (err: unknown) => err instanceof DOMException && err.name === 'AbortError',
|
||||
}));
|
||||
|
||||
const mockToastError = vi.fn();
|
||||
vi.mock('../components/ui/sonner', () => ({
|
||||
toast: {
|
||||
error: (...args: unknown[]) => mockToastError(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
function createConversation(): Conversation {
|
||||
return {
|
||||
type: 'contact',
|
||||
@@ -55,6 +63,7 @@ describe('useConversationMessages ACK ordering', () => {
|
||||
beforeEach(() => {
|
||||
mockGetMessages.mockReset();
|
||||
messageCache.clear();
|
||||
mockToastError.mockReset();
|
||||
});
|
||||
|
||||
it('applies buffered ACK when message is added after ACK event', async () => {
|
||||
@@ -447,6 +456,55 @@ describe('useConversationMessages older-page dedup and reentry', () => {
|
||||
expect(result.current.messages.filter((msg) => msg.id === 0)).toHaveLength(1);
|
||||
expect(result.current.messages).toHaveLength(201);
|
||||
});
|
||||
|
||||
it('aborts stale older-page requests on conversation switch without toasting', async () => {
|
||||
const convA: Conversation = { type: 'contact', id: 'conv_a', name: 'Contact A' };
|
||||
const convB: Conversation = { type: 'contact', id: 'conv_b', name: 'Contact B' };
|
||||
|
||||
const fullPage = Array.from({ length: 200 }, (_, i) =>
|
||||
createMessage({
|
||||
id: i + 1,
|
||||
conversation_key: 'conv_a',
|
||||
text: `msg-${i + 1}`,
|
||||
sender_timestamp: 1700000000 + i,
|
||||
received_at: 1700000000 + i,
|
||||
})
|
||||
);
|
||||
mockGetMessages.mockResolvedValueOnce(fullPage);
|
||||
|
||||
const olderDeferred = createDeferred<Message[]>();
|
||||
let olderSignal: AbortSignal | undefined;
|
||||
mockGetMessages.mockImplementationOnce((_, signal?: AbortSignal) => {
|
||||
olderSignal = signal;
|
||||
signal?.addEventListener('abort', () => {
|
||||
olderDeferred.resolve([]);
|
||||
});
|
||||
return new Promise<Message[]>((_, reject) => {
|
||||
signal?.addEventListener('abort', () => {
|
||||
reject(new DOMException('The operation was aborted', 'AbortError'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ conv }: { conv: Conversation }) => useConversationMessages(conv),
|
||||
{ initialProps: { conv: convA } }
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.messagesLoading).toBe(false));
|
||||
act(() => {
|
||||
void result.current.fetchOlderMessages();
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.loadingOlder).toBe(true));
|
||||
|
||||
mockGetMessages.mockResolvedValueOnce([createMessage({ id: 999, conversation_key: 'conv_b' })]);
|
||||
rerender({ conv: convB });
|
||||
|
||||
await waitFor(() => expect(result.current.messagesLoading).toBe(false));
|
||||
expect(olderSignal?.aborted).toBe(true);
|
||||
expect(mockToastError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useConversationMessages forward pagination', () => {
|
||||
@@ -454,6 +512,7 @@ describe('useConversationMessages forward pagination', () => {
|
||||
mockGetMessages.mockReset();
|
||||
mockGetMessagesAround.mockReset();
|
||||
messageCache.clear();
|
||||
mockToastError.mockReset();
|
||||
});
|
||||
|
||||
it('fetchNewerMessages loads newer messages and appends them', async () => {
|
||||
@@ -618,6 +677,69 @@ describe('useConversationMessages forward pagination', () => {
|
||||
expect(result.current.messages[0].text).toBe('latest-msg');
|
||||
});
|
||||
|
||||
it('aborts stale newer-page requests on conversation switch without toasting', async () => {
|
||||
const convA: Conversation = { type: 'channel', id: 'ch1', name: 'Channel A' };
|
||||
const convB: Conversation = { type: 'channel', id: 'ch2', name: 'Channel B' };
|
||||
|
||||
mockGetMessagesAround.mockResolvedValueOnce({
|
||||
messages: [
|
||||
createMessage({
|
||||
id: 1,
|
||||
type: 'CHAN',
|
||||
conversation_key: 'ch1',
|
||||
text: 'msg-0',
|
||||
sender_timestamp: 1700000000,
|
||||
received_at: 1700000000,
|
||||
}),
|
||||
],
|
||||
has_older: false,
|
||||
has_newer: true,
|
||||
});
|
||||
|
||||
let newerSignal: AbortSignal | undefined;
|
||||
mockGetMessages.mockImplementationOnce((_, signal?: AbortSignal) => {
|
||||
newerSignal = signal;
|
||||
return new Promise<Message[]>((_, reject) => {
|
||||
signal?.addEventListener('abort', () => {
|
||||
reject(new DOMException('The operation was aborted', 'AbortError'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const initialProps: { conv: Conversation; target: number | null } = {
|
||||
conv: convA,
|
||||
target: 1,
|
||||
};
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ conv, target }: { conv: Conversation; target: number | null }) =>
|
||||
useConversationMessages(conv, target),
|
||||
{ initialProps }
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.messagesLoading).toBe(false));
|
||||
|
||||
act(() => {
|
||||
void result.current.fetchNewerMessages();
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.loadingNewer).toBe(true));
|
||||
|
||||
mockGetMessages.mockResolvedValueOnce([
|
||||
createMessage({
|
||||
id: 999,
|
||||
type: 'CHAN',
|
||||
conversation_key: 'ch2',
|
||||
text: 'conv-b',
|
||||
}),
|
||||
]);
|
||||
rerender({ conv: convB, target: null });
|
||||
|
||||
await waitFor(() => expect(result.current.messagesLoading).toBe(false));
|
||||
expect(newerSignal?.aborted).toBe(true);
|
||||
expect(mockToastError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('preserves around-loaded messages when the jump target is cleared in the same conversation', async () => {
|
||||
const conv: Conversation = { type: 'channel', id: 'ch1', name: 'Channel' };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user