feat(video): clear center buffering spinner over in-chat videos
The native controls' buffering indicator sits by the timer and is easy to miss. Wrap the <video> and overlay a centered spinner shown while it buffers (waiting/seeking/stalled -> show; playing/canplay/seeked/loadeddata -> hide). Media events don't bubble, so the listeners run in the capture phase; the overlay is pointer-events:none so it never blocks the native controls. build batch158.
This commit is contained in:
+17
-6
@@ -661,10 +661,13 @@
|
||||
.convo-msgs img,.bubble img{max-height:340px;max-width:100%;height:auto;}
|
||||
.att-file{display:inline-flex;align-items:center;gap:.4rem;background:rgba(0,0,0,.06);border:1px solid var(--line);border-radius:8px;padding:.4rem .6rem;color:inherit;text-decoration:none;font-size:.85rem;margin:.15rem 0;max-width:240px;}
|
||||
.att-file span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||||
/* In-chat video: a plain native <video controls> — shows a poster frame + the OS play button, plays inline
|
||||
on one tap (playsinline), streams once and is cached by the browser (no re-download on replay). Fixed box
|
||||
size so loading the poster can't reflow / "dance" the thread. */
|
||||
.att-vid{width:min(260px,74vw);max-height:340px;border-radius:10px;display:block;margin:.15rem 0;background:#000;object-fit:contain;}
|
||||
/* In-chat video: native <video controls> (poster from /thumbs, plays inline on one tap, HTTP-Range streamed).
|
||||
Wrapped so we can overlay a clear CENTER spinner while it buffers — the OS controls' own buffering hint is
|
||||
small/unclear. The spinner is display-only (pointer-events:none) so it never blocks the controls. */
|
||||
.att-vid{position:relative;width:min(260px,74vw);max-height:340px;border-radius:10px;overflow:hidden;display:block;margin:.15rem 0;background:#000;line-height:0;}
|
||||
.att-vid-v{display:block;width:100%;max-height:340px;background:#000;object-fit:contain;}
|
||||
.att-vid-spin{position:absolute;top:50%;left:50%;width:46px;height:46px;margin:-23px 0 0 -23px;border-radius:50%;border:3px solid rgba(255,255,255,.35);border-top-color:#fff;opacity:0;pointer-events:none;z-index:3;}
|
||||
.att-vid.buffering .att-vid-spin{opacity:1;animation:ptrspin .8s linear infinite;}
|
||||
.bubble.mine .att-file{background:rgba(255,255,255,.18);border-color:rgba(255,255,255,.3);}
|
||||
.att-file .att-sz{opacity:.65;font-size:.75rem;flex:0 0 auto;}
|
||||
/* groups */
|
||||
@@ -1162,7 +1165,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<script src="/icons.js?v=6"></script>
|
||||
<script>window.__BUILD='2026-07-22-batch157';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||
<script>window.__BUILD='2026-07-22-batch158';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||
// Emoji are rendered with the OS's own (colour) emoji font — instant, zero network.
|
||||
//
|
||||
// We used to run Twemoji over every emoji, which swapped each one for an <img> pulled INDIVIDUALLY from
|
||||
@@ -2010,7 +2013,7 @@ function bubbleHTML(m){
|
||||
const A = m.attachment;
|
||||
const att = A ? (
|
||||
A.isImage ? '<img class="att-img" src="/files/'+pEsc(A.id)+'" data-img="/files/'+pEsc(A.id)+'" alt="'+pEsc(A.name)+'" title="Click to view">'
|
||||
: A.isVideo ? '<video class="att-vid" src="/files/'+pEsc(A.id)+'#t=0.1" poster="/thumbs/'+pEsc(A.id)+'" controls playsinline preload="metadata" title="'+pEsc(A.name)+'"></video>'
|
||||
: A.isVideo ? '<div class="att-vid"><video class="att-vid-v" src="/files/'+pEsc(A.id)+'#t=0.1" poster="/thumbs/'+pEsc(A.id)+'" controls playsinline preload="metadata" title="'+pEsc(A.name)+'"></video><span class="att-vid-spin" aria-hidden="true"></span></div>'
|
||||
: '<a class="att-file" href="/files/'+pEsc(A.id)+'" download="'+pEsc(A.name)+'">'+ic('file',15)+' <span>'+pEsc(A.name)+'</span> <span class="att-sz">'+fmtSize(A.size)+'</span></a>') : '';
|
||||
const mentionsMe=convoIsGroup && !mine && Array.isArray(m.mentions) && (m.mentions.includes(ME.id)||m.mentions.includes('everyone'));
|
||||
// DM ticks: sent (1 grey) → delivered (2 grey) → read (2 yellow).
|
||||
@@ -5038,6 +5041,14 @@ window.addEventListener('popstate', ()=>{ if(bzcBack()){ try{ history.pushState(
|
||||
if((t.clientX-sx)>60 && Math.abs(t.clientY-sy)<50) bzcBack(); // rightward, mostly-horizontal swipe = back
|
||||
}, {passive:true});
|
||||
})();
|
||||
// In-chat video buffering: show a clear CENTER spinner while a video buffers (the OS controls' own indicator
|
||||
// is small/unclear). Media events (waiting/playing/…) do NOT bubble, so we listen in the CAPTURE phase. This
|
||||
// only toggles a class — it never intercepts taps, so the native controls keep working.
|
||||
(function(){
|
||||
const set=(on)=>(e)=>{ const v=e.target; if(v && v.classList && v.classList.contains('att-vid-v')){ const w=v.parentElement; if(w && w.classList.contains('att-vid')) w.classList.toggle('buffering', on); } };
|
||||
['waiting','seeking','stalled'].forEach(ev=>document.addEventListener(ev, set(true), true));
|
||||
['playing','canplay','seeked','pause','ended','error','loadeddata'].forEach(ev=>document.addEventListener(ev, set(false), true));
|
||||
})();
|
||||
// #9: swipe a message bubble to the right to reply to it (mobile), like WhatsApp/Teams. The bubble
|
||||
// follows the finger a little; releasing past the threshold opens the reply composer for that message.
|
||||
(function(){
|
||||
|
||||
Reference in New Issue
Block a user