1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
// Pull down at the top of the page to reload it — where the browser doesn't.
//
// Chrome's own pull-to-refresh is off whenever the page runs full screen,
// which the Android app (a TWA in immersive mode) always is, and a WebView
// (the Beta and F-Droid builds) and an installed PWA have none at all: "pull
// down to refresh isn't working for me in the apk" (Adam, 2026-11-06). In
// those, this one takes over, and the browser's is switched off so the two
// never both fire; a plain browser tab keeps the browser's.
//
// A pull counts only when the page is at its top and the finger isn't in
// something scrolled down (a chat, a picker) or something over the page (a
// dialog, the media viewer): those keep their own drags. It's vertical only —
// sideways swipes belong to the tab navigation.
const PULL = 80; // px of (damped) pull that reloads on release
const MAX = 120;
export function appLike() {
try {
if (typeof window !== 'undefined' && window.CoinosHost) return true;
return matchMedia('(display-mode: standalone)').matches || matchMedia('(display-mode: fullscreen)').matches;
} catch { return false; }
}
// something under the finger that is scrolled away from its top, or a layer
// over the page, owns this drag
function blocked(el) {
for (let n = el; n && n !== document.body && n !== document.documentElement; n = n.parentElement) {
if (n.scrollTop > 0) return true;
if (n.matches && n.matches('.lightbox, .confirm-pop-backdrop, .msg-sheet, [role=dialog], dialog, input, textarea, select, [contenteditable=true]')) return true;
}
return false;
}
export function initPullRefresh({ reload = () => location.reload() } = {}) {
if (typeof document === 'undefined' || !appLike()) return false;
document.documentElement.style.overscrollBehaviorY = 'contain'; // the browser's off: this one instead
const ind = document.createElement('div');
ind.className = 'ptr';
ind.setAttribute('aria-hidden', 'true');
ind.innerHTML = '<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/></svg>';
document.body.appendChild(ind);
let y0 = null, x0 = 0, dy = 0, active = false, done = false;
const show = (d) => {
const p = Math.min(1, d / PULL);
ind.style.transform = `translate(-50%, ${Math.min(d, MAX) - 48}px) rotate(${p * 270}deg)`;
ind.style.opacity = String(Math.min(1, p * 1.2));
ind.classList.toggle('ready', d >= PULL);
};
const reset = () => {
ind.classList.remove('ready');
ind.style.transition = 'transform .2s ease, opacity .2s ease';
ind.style.transform = 'translate(-50%, -48px)';
ind.style.opacity = '0';
setTimeout(() => { ind.style.transition = ''; }, 220);
};
addEventListener('touchstart', (e) => {
if (done || e.touches.length !== 1) { y0 = null; return; }
const top = (document.scrollingElement || document.documentElement).scrollTop <= 0;
if (!top || blocked(e.target)) { y0 = null; return; }
y0 = e.touches[0].clientY; x0 = e.touches[0].clientX; dy = 0; active = false;
}, { passive: true });
addEventListener('touchmove', (e) => {
if (y0 == null || done) return;
const t = e.touches[0];
const ry = t.clientY - y0, rx = t.clientX - x0;
if (!active) {
// decide once: a clear downward pull, not a sideways swipe or a scroll up
if (Math.abs(ry) < 8 && Math.abs(rx) < 8) return;
if (ry <= 0 || Math.abs(rx) > ry) { y0 = null; return; }
active = true;
}
dy = ry * 0.5; // damped, like the browser's
show(dy);
}, { passive: true });
const end = () => {
if (y0 == null) return;
y0 = null;
if (active && dy >= PULL) {
done = true;
ind.classList.add('spin');
ind.style.transform = 'translate(-50%, 24px)';
reload();
return;
}
if (active) reset();
active = false;
};
addEventListener('touchend', end, { passive: true });
addEventListener('touchcancel', () => { if (y0 != null) { y0 = null; if (active) reset(); active = false; } }, { passive: true });
return true;
}
|