Add new message sending scroll-to-bottom when near

This commit is contained in:
Jack Kingsman committed 2026-01-12 23:59:36 -08:00
1 parent 05508b7f8a
commit 3230d29fbe
3 files changed
+10 -6

No files matched your search

@@ -77,7 +77,7 @@ No ACL entries`;const o=[`ACL (${a.length})`];for(const r of a){const s=r.name||
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/const Ek=[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]],Yg=Nu("x",Ek);var Zg=Wg();const Xg=Fr(Zg);function Sk(a){if(typeof document>"u")return;let o=document.head||document.getElementsByTagName("head")[0],r=document.createElement("style");r.type="text/css",o.appendChild(r),r.styleSheet?r.styleSheet.cssText=a:r.appendChild(document.createTextNode(a))}const Bk=a=>{switch(a){case"success":return Fk;case"info":return Tk;case"warning":return Rk;case"error":return Pk;default:return null}},_k=Array(12).fill(0),Dk=({visible:a,className:o})=>de.createElement("div",{className:["sonner-loading-wrapper",o].filter(Boolean).join(" "),"data-visible":a},de.createElement("div",{className:"sonner-spinner"},_k.map((r,s)=>de.createElement("div",{className:"sonner-loading-bar",key:`spinner-bar-${s}`})))),Fk=de.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor",height:"20",width:"20"},de.createElement("path",{fillRule:"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z",clipRule:"evenodd"})),Rk=de.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",height:"20",width:"20"},de.createElement("path",{fillRule:"evenodd",d:"M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z",clipRule:"evenodd"})),Tk=de.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor",height:"20",width:"20"},de.createElement("path",{fillRule:"evenodd",d:"M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z",clipRule:"evenodd"})),Pk=de.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor",height:"20",width:"20"},de.createElement("path",{fillRule:"evenodd",d:"M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z",clipRule:"evenodd"})),Nk=de.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"},de.createElement("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),de.createElement("line",{x1:"6",y1:"6",x2:"18",y2:"18"})),Ik=()=>{const[a,o]=de.useState(document.hidden);return de.useEffect(()=>{const r=()=>{o(document.hidden)};return document.addEventListener("visibilitychange",r),()=>window.removeEventListener("visibilitychange",r)},[]),a};let zu=1;class Mk{constructor(){this.subscribe=o=>(this.subscribers.push(o),()=>{const r=this.subscribers.indexOf(o);this.subscribers.splice(r,1)}),this.publish=o=>{this.subscribers.forEach(r=>r(o))},this.addToast=o=>{this.publish(o),this.toasts=[...this.toasts,o]},this.create=o=>{var r;const{message:s,...u}=o,l=typeof(o==null?void 0:o.id)=="number"||((r=o.id)==null?void 0:r.length)>0?o.id:zu++,m=this.toasts.find(h=>h.id===l),c=o.dismissible===void 0?!0:o.dismissible;return this.dismissedToasts.has(l)&&this.dismissedToasts.delete(l),m?this.toasts=this.toasts.map(h=>h.id===l?(this.publish({...h,...o,id:l,title:s}),{...h,...o,id:l,dismissible:c,title:s}):h):this.addToast({title:s,...u,dismissible:c,id:l}),l},this.dismiss=o=>(o?(this.dismissedToasts.add(o),requestAnimationFrame(()=>this.subscribers.forEach(r=>r({id:o,dismiss:!0})))):this.toasts.forEach(r=>{this.subscribers.forEach(s=>s({id:r.id,dismiss:!0}))}),o),this.message=(o,r)=>this.create({...r,message:o}),this.error=(o,r)=>this.create({...r,message:o,type:"error"}),this.success=(o,r)=>this.create({...r,type:"success",message:o}),this.info=(o,r)=>this.create({...r,type:"info",message:o}),this.warning=(o,r)=>this.create({...r,type:"warning",message:o}),this.loading=(o,r)=>this.create({...r,type:"loading",message:o}),this.promise=(o,r)=>{if(!r)return;let s;r.loading!==void 0&&(s=this.create({...r,promise:o,type:"loading",message:r.loading,description:typeof r.description!="function"?r.description:void 0}));const u=Promise.resolve(o instanceof Function?o():o);let l=s!==void 0,m;const c=u.then(async d=>{if(m=["resolve",d],de.isValidElement(d))l=!1,this.create({id:s,type:"default",message:d});else if(Lk(d)&&!d.ok){l=!1;const y=typeof r.error=="function"?await r.error(`HTTP error! status: ${d.status}`):r.error,f=typeof r.description=="function"?await r.description(`HTTP error! status: ${d.status}`):r.description,w=typeof y=="object"&&!de.isValidElement(y)?y:{message:y};this.create({id:s,type:"error",description:f,...w})}else if(d instanceof Error){l=!1;const y=typeof r.error=="function"?await r.error(d):r.error,Line truncated
*/const Ek=[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]],Yg=Nu("x",Ek);var Zg=Wg();const Xg=Fr(Zg);function Sk(a){if(typeof document>"u")return;let o=document.head||document.getElementsByTagName("head")[0],r=document.createElement("style");r.type="text/css",o.appendChild(r),r.styleSheet?r.styleSheet.cssText=a:r.appendChild(document.createTextNode(a))}const Bk=a=>{switch(a){case"success":return Fk;case"info":return Tk;case"warning":return Rk;case"error":return Pk;default:return null}},_k=Array(12).fill(0),Dk=({visible:a,className:o})=>de.createElement("div",{className:["sonner-loading-wrapper",o].filter(Boolean).join(" "),"data-visible":a},de.createElement("div",{className:"sonner-spinner"},_k.map((r,s)=>de.createElement("div",{className:"sonner-loading-bar",key:`spinner-bar-${s}`})))),Fk=de.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor",height:"20",width:"20"},de.createElement("path",{fillRule:"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z",clipRule:"evenodd"})),Rk=de.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",height:"20",width:"20"},de.createElement("path",{fillRule:"evenodd",d:"M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z",clipRule:"evenodd"})),Tk=de.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor",height:"20",width:"20"},de.createElement("path",{fillRule:"evenodd",d:"M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z",clipRule:"evenodd"})),Pk=de.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor",height:"20",width:"20"},de.createElement("path",{fillRule:"evenodd",d:"M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z",clipRule:"evenodd"})),Nk=de.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"},de.createElement("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),de.createElement("line",{x1:"6",y1:"6",x2:"18",y2:"18"})),Ik=()=>{const[a,o]=de.useState(document.hidden);return de.useEffect(()=>{const r=()=>{o(document.hidden)};return document.addEventListener("visibilitychange",r),()=>window.removeEventListener("visibilitychange",r)},[]),a};let zu=1;class Mk{constructor(){this.subscribe=o=>(this.subscribers.push(o),()=>{const r=this.subscribers.indexOf(o);this.subscribers.splice(r,1)}),this.publish=o=>{this.subscribers.forEach(r=>r(o))},this.addToast=o=>{this.publish(o),this.toasts=[...this.toasts,o]},this.create=o=>{var r;const{message:s,...u}=o,l=typeof(o==null?void 0:o.id)=="number"||((r=o.id)==null?void 0:r.length)>0?o.id:zu++,m=this.toasts.find(h=>h.id===l),c=o.dismissible===void 0?!0:o.dismissible;return this.dismissedToasts.has(l)&&this.dismissedToasts.delete(l),m?this.toasts=this.toasts.map(h=>h.id===l?(this.publish({...h,...o,id:l,title:s}),{...h,...o,id:l,dismissible:c,title:s}):h):this.addToast({title:s,...u,dismissible:c,id:l}),l},this.dismiss=o=>(o?(this.dismissedToasts.add(o),requestAnimationFrame(()=>this.subscribers.forEach(r=>r({id:o,dismiss:!0})))):this.toasts.forEach(r=>{this.subscribers.forEach(s=>s({id:r.id,dismiss:!0}))}),o),this.message=(o,r)=>this.create({...r,message:o}),this.error=(o,r)=>this.create({...r,message:o,type:"error"}),this.success=(o,r)=>this.create({...r,type:"success",message:o}),this.info=(o,r)=>this.create({...r,type:"info",message:o}),this.warning=(o,r)=>this.create({...r,type:"warning",message:o}),this.loading=(o,r)=>this.create({...r,type:"loading",message:o}),this.promise=(o,r)=>{if(!r)return;let s;r.loading!==void 0&&(s=this.create({...r,promise:o,type:"loading",message:r.loading,description:typeof r.description!="function"?r.description:void 0}));const u=Promise.resolve(o instanceof Function?o():o);let l=s!==void 0,m;const c=u.then(async d=>{if(m=["resolve",d],de.isValidElement(d))l=!1,this.create({id:s,type:"default",message:d});else if(Lk(d)&&!d.ok){l=!1;const y=typeof r.error=="function"?await r.error(`HTTP error! status: ${d.status}`):r.error,f=typeof r.description=="function"?await r.description(`HTTP error! status: ${d.status}`):r.description,w=typeof y=="object"&&!de.isValidElement(y)?y:{message:y};this.create({id:s,type:"error",description:f,...w})}else if(d instanceof Error){l=!1;const y=typeof r.error=="function"?await r.error(d):r.error,Line truncated
`).map((A,I,L)=>B.jsxs("span",{children:[uz(A,c),I<L.length-1&&B.jsx("br",{})]},I)),!Z&&B.jsx("span",{className:"text-[10px] text-muted-foreground/50 ml-2",children:th(C.sender_timestamp||C.received_at)}),C.outgoing&&(C.acked>0?` ✓${C.acked>1?C.acked:""}`:" ?")]})]})]},C.id)})]}),y&&B.jsx("button",{onClick:k,className:"absolute bottom-4 right-4 w-10 h-10 rounded-full bg-muted hover:bg-accent border border-border flex items-center justify-center shadow-lg transition-opacity",title:"Scroll to bottom",children:B.jsx("svg",{xmlns:"http://www.w3.org/2000/svg",width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",className:"text-muted-foreground",children:B.jsx("polyline",{points:"6 9 12 15 18 9"})})})]})}const lh=156,pz=140,mz=156,hz=120,gz=8,yz=E.forwardRef(function({onSend:o,disabled:r,placeholder:s,isRepeaterMode:u,conversationType:l,senderName:m},c){const[h,d]=E.useState(""),[g,y]=E.useState(!1),f=E.useRef(null);E.useImperativeHandle(c,()=>({appendText:C=>{var D;d(R=>R+C),(D=f.current)==null||D.focus()}}));const z=E.useMemo(()=>{if(l==="contact")return{warningAt:pz,dangerAt:lh,hardLimit:lh};if(l==="channel"){const C=(m==null?void 0:m.length)??10,D=Math.max(1,mz-C-2);return{warningAt:hz,dangerAt:Math.max(1,D-gz),hardLimit:D}}return null},[l,m]),{limitState:w,warningMessage:k}=E.useMemo(()=>{if(!z)return{limitState:"normal",warningMessage:null};const C=h.length;return C>=z.hardLimit?{limitState:"error",warningMessage:"likely truncated by radio"}:C>=z.dangerAt?{limitState:"danger",warningMessage:"may impact multi-repeater hop delivery"}:C>=z.warningAt?{limitState:"warning",warningMessage:"may impact multi-repeater hop delivery"}:{limitState:"normal",warningMessage:null}},[h.length,z]),v=z?z.hardLimit-h.length:0,j=E.useCallback(async C=>{C.preventDefault();const D=h.trim();if(u){if(g||r)return;const R=D==="."?"":D;y(!0);try{await o(R),d("")}catch(F){console.error("Failed to request telemetry:",F);return}finally{y(!1)}setTimeout(()=>{var F;return(F=f.current)==null?void 0:F.focus()},0)}else{if(!D||g||r)return;y(!0);try{await o(D),d("")}catch(R){console.error("Failed to send message:",R);return}finally{y(!1)}setTimeout(()=>{var R;return(R=f.current)==null?void 0:R.focus()},0)}},[h,g,r,o,u]),q=E.useCallback(C=>{C.key==="Enter"&&!C.shiftKey&&(C.preventDefault(),j(C))},[j]),x=u?h.trim().length>0||h===".":h.trim().length>0,_=!u&&z!==null;return B.jsxs("form",{className:"px-4 py-3 border-t border-border flex flex-col gap-1",onSubmit:j,children:[B.jsxs("div",{className:"flex gap-2",children:[B.jsx(xi,{ref:f,type:u?"password":"text",value:h,onChange:C=>d(C.target.value),onKeyDown:q,placeholder:s||(u?"Enter password (or . for none)...":"Type a message..."),disabled:r||g,className:"flex-1 min-w-0"}),B.jsx(Ha,{type:"submit",disabled:r||g||!x,className:"flex-shrink-0",children:g?u?"Fetching...":"Sending...":u?"Fetch":"Send"})]}),_&&B.jsxs("div",{className:"flex items-center justify-end gap-2 text-xs",children:[B.jsxs("span",{className:$e("tabular-nums",w==="error"||w==="danger"?"text-red-500 font-medium":w==="warning"?"text-yellow-500":"text-muted-foreground"),children:[h.length,"/",z.hardLimit,v<0&&` (${v})`]}),k&&B.jsxs("span",{className:$e(w==="error"?"text-red-500":"text-yellow-500"),children:["— ",k]})]})]})});function Si(a,o,{checkForDefaultPrevented:r=!0}={}){return function(u){if(a==null||a(u),r===!1||!u.defaultPrevented)return o==null?void 0:o(u)}}function bz(a,o){const r=E.createContext(o),s=l=>{const{children:m,...c}=l,h=E.useMemo(()=>c,Object.values(c));return B.jsx(r.Provider,{value:h,children:m})};s.displayName=a+"Provider";function u(l){const m=E.useContext(r);if(m)return m;if(o!==void 0)return o;throw new Error(`\`${l}\` must be used within \`${a}\``)}return[s,u]}function Ho(a,o=[]){let r=[];function s(l,m){const c=E.createContext(m),h=r.length;r=[...r,m];const d=y=>{var j;const{scope:f,children:z,...w}=y,k=((j=f==null?void 0:f[a])==null?void 0:j[h])||c,v=E.useMemo(()=>w,Object.values(w));return B.jsx(k.Provider,{value:v,children:z})};d.displayName=l+"Provider";function g(y,f){var k;const z=((k=f==null?void 0:f[a])==null?void 0:k[h])||c,w=E.useContext(z);if(w)return w;if(m!==void 0)return m;throw new Error(`\`${y}\` must be used within \`${l}\``)}return[d,g]}const u=()=>{const l=r.map(m=>E.createContext(m));return function(c){const h=(c==null?void 0:c[a])||l;return E.useMemo(()=>({[`__scope${a}`]:{...c,[a]:h}}),[c,h])}};return u.scopeName=a,[s,fz(u,...o)]}function fz(...a){const o=a[0];if(a.length===1)return o;const r=()=>{const s=a.map(u=>({useScope:u(),scopeName:u.scopeName}));return function(l){const m=s.reduce((c,{useScope:h,scopeName:d})=>{const y=h(l)[`__scope${d}`];return{...c,...y}},{});return E.useMemo(()=>({[`__scope${o.scopeName}`]:m}),[m])}};return r.scopeName=o.scopeName,r}var _r=globalThis!=null&&globalThis.document?E.useLayoutEffect:()=>{},vz=Pu[" useId ".trim().toString()]||(()=>{}),kz=0;function No(a){conLine truncated
.`.concat(Jz,` {
overflow: hidden `).concat(s,`;
+1 -1
View File
@@ -13,7 +13,7 @@
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<script type="module" crossorigin src="/assets/index-CvVcf00q.js"></script>
<script type="module" crossorigin src="/assets/index-DVwayj9K.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DaLCXB8p.css">
</head>
<body>
+8 -4
View File
@@ -80,7 +80,9 @@ export function MessageList({
const scrollStateRef = useRef({
scrollTop: 0,
scrollHeight: 0,
clientHeight: 0,
wasNearTop: false,
wasNearBottom: true, // Default to true so initial messages scroll to bottom
});
// Handle scroll position AFTER render
@@ -101,8 +103,8 @@ export function MessageList({
if (scrollStateRef.current.wasNearTop && scrollHeightDiff > 0) {
// User was near top (loading older) - preserve position by adding the height diff
list.scrollTop = scrollStateRef.current.scrollTop + scrollHeightDiff;
} else if (!scrollStateRef.current.wasNearTop) {
// User was at bottom - scroll to bottom for new messages
} else if (scrollStateRef.current.wasNearBottom) {
// User was near bottom - scroll to bottom for new messages (including sent)
list.scrollTop = list.scrollHeight;
}
}
@@ -115,7 +117,7 @@ export function MessageList({
if (messages.length === 0) {
isInitialLoadRef.current = true;
prevMessagesLengthRef.current = 0;
scrollStateRef.current = { scrollTop: 0, scrollHeight: 0, wasNearTop: false };
scrollStateRef.current = { scrollTop: 0, scrollHeight: 0, clientHeight: 0, wasNearTop: false, wasNearBottom: true };
}
}, [messages.length]);
@@ -124,16 +126,18 @@ export function MessageList({
if (!listRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = listRef.current;
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
// Always capture current scroll state (needed for scroll preservation)
scrollStateRef.current = {
scrollTop,
scrollHeight,
clientHeight,
wasNearTop: scrollTop < 150,
wasNearBottom: distanceFromBottom < 100,
};
// Show scroll-to-bottom button when not near the bottom (more than 100px away)
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
setShowScrollToBottom(distanceFromBottom > 100);
if (!onLoadOlder || loadingOlder || !hasOlderMessages) return;