Add real-time listeners and pull-to-refresh
This commit is contained in:
+107
-28
@@ -864,7 +864,24 @@
|
|||||||
.tab-btn { font-size: 12px; padding: 8px 2px; }
|
.tab-btn { font-size: 12px; padding: 8px 2px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── SPINNER ── */
|
/* ── PULL TO REFRESH ── */
|
||||||
|
.ptr-indicator {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(-48px);
|
||||||
|
background: var(--surface2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--teal);
|
||||||
|
z-index: 200;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
pointer-events: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.ptr-indicator.visible { transform: translateX(-50%) translateY(12px); }
|
||||||
.spinner {
|
.spinner {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
@@ -1218,6 +1235,8 @@
|
|||||||
|
|
||||||
</div><!-- /app -->
|
</div><!-- /app -->
|
||||||
|
|
||||||
|
<div id="ptr-indicator" class="ptr-indicator">↓ Refreshing…</div>
|
||||||
|
|
||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
|
|
||||||
<!-- Firebase SDK -->
|
<!-- Firebase SDK -->
|
||||||
@@ -1567,49 +1586,79 @@ function todayKey() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
// FRIENDS SYSTEM
|
// FRIENDS SYSTEM — REAL-TIME LISTENERS
|
||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
let friendsList = [];
|
let friendsList = [];
|
||||||
let pendingReceived = [];
|
let pendingReceived = [];
|
||||||
let pendingSent = [];
|
let pendingSent = [];
|
||||||
|
let friendUnsubscribers = [];
|
||||||
|
|
||||||
async function loadFriends() {
|
function teardownListeners() {
|
||||||
|
friendUnsubscribers.forEach(fn => fn());
|
||||||
|
friendUnsubscribers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFriends() {
|
||||||
if (!currentUser) return;
|
if (!currentUser) return;
|
||||||
|
teardownListeners();
|
||||||
const uid = currentUser.uid;
|
const uid = currentUser.uid;
|
||||||
|
|
||||||
const frRef = collection(db, 'friendRequests');
|
const frRef = collection(db, 'friendRequests');
|
||||||
const q1 = query(frRef, where('toUid', '==', uid));
|
|
||||||
const q2 = query(frRef, where('fromUid', '==', uid));
|
|
||||||
|
|
||||||
const [receivedSnap, sentSnap] = await Promise.all([getDocs(q1), getDocs(q2)]);
|
const unsubOwn = onSnapshot(doc(db, 'users', uid), snap => {
|
||||||
|
if (!snap.exists()) return;
|
||||||
|
currentUserData = { ...currentUserData, ...snap.data() };
|
||||||
|
renderMySteps();
|
||||||
|
checkSetupReminder();
|
||||||
|
});
|
||||||
|
friendUnsubscribers.push(unsubOwn);
|
||||||
|
|
||||||
pendingReceived = receivedSnap.docs
|
const unsubReceived = onSnapshot(query(frRef, where('toUid', '==', uid)), async receivedSnap => {
|
||||||
.filter(d => d.data().status === 'pending')
|
const unsubSent = onSnapshot(query(frRef, where('fromUid', '==', uid)), async sentSnap => {
|
||||||
.map(d => ({ id: d.id, ...d.data() }));
|
pendingReceived = receivedSnap.docs
|
||||||
|
.filter(d => d.data().status === 'pending')
|
||||||
|
.map(d => ({ id: d.id, ...d.data() }));
|
||||||
|
|
||||||
pendingSent = sentSnap.docs
|
pendingSent = sentSnap.docs
|
||||||
.filter(d => d.data().status === 'pending')
|
.filter(d => d.data().status === 'pending')
|
||||||
.map(d => ({ id: d.id, ...d.data() }));
|
.map(d => ({ id: d.id, ...d.data() }));
|
||||||
|
|
||||||
const confirmedFromMe = sentSnap.docs
|
const confirmedFromMe = sentSnap.docs
|
||||||
.filter(d => d.data().status === 'accepted')
|
.filter(d => d.data().status === 'accepted')
|
||||||
.map(d => d.data().toUid);
|
.map(d => d.data().toUid);
|
||||||
|
|
||||||
const confirmedToMe = receivedSnap.docs
|
const confirmedToMe = receivedSnap.docs
|
||||||
.filter(d => d.data().status === 'accepted')
|
.filter(d => d.data().status === 'accepted')
|
||||||
.map(d => d.data().fromUid);
|
.map(d => d.data().fromUid);
|
||||||
|
|
||||||
const allFriendUids = [...new Set([...confirmedFromMe, ...confirmedToMe])];
|
const allFriendUids = [...new Set([...confirmedFromMe, ...confirmedToMe])];
|
||||||
|
|
||||||
friendsList = [];
|
friendUnsubscribers.filter(fn => fn._isFriendProfile).forEach(fn => fn());
|
||||||
for (const fuid of allFriendUids) {
|
friendUnsubscribers = friendUnsubscribers.filter(fn => !fn._isFriendProfile);
|
||||||
const snap = await getDoc(doc(db, 'profiles', fuid));
|
|
||||||
if (snap.exists()) friendsList.push({ uid: fuid, ...snap.data() });
|
|
||||||
}
|
|
||||||
|
|
||||||
renderFriendSteps();
|
friendsList = [];
|
||||||
renderFriendsPanel();
|
for (const fuid of allFriendUids) {
|
||||||
updateBadge();
|
const unsubProfile = onSnapshot(doc(db, 'profiles', fuid), profileSnap => {
|
||||||
|
if (!profileSnap.exists()) return;
|
||||||
|
const idx = friendsList.findIndex(f => f.uid === fuid);
|
||||||
|
const data = { uid: fuid, ...profileSnap.data() };
|
||||||
|
if (idx >= 0) {
|
||||||
|
friendsList[idx] = data;
|
||||||
|
} else {
|
||||||
|
friendsList.push(data);
|
||||||
|
}
|
||||||
|
renderFriendSteps();
|
||||||
|
renderFriendsPanel();
|
||||||
|
});
|
||||||
|
unsubProfile._isFriendProfile = true;
|
||||||
|
friendUnsubscribers.push(unsubProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFriendsPanel();
|
||||||
|
updateBadge();
|
||||||
|
});
|
||||||
|
friendUnsubscribers.push(unsubSent);
|
||||||
|
});
|
||||||
|
friendUnsubscribers.push(unsubReceived);
|
||||||
}
|
}
|
||||||
|
|
||||||
function avatarHtml(f, size=44) {
|
function avatarHtml(f, size=44) {
|
||||||
@@ -1867,9 +1916,11 @@ window.saveProfile = async function() {
|
|||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
window.signOut = async function() {
|
window.signOut = async function() {
|
||||||
if (!confirm('Sign out?')) return;
|
if (!confirm('Sign out?')) return;
|
||||||
|
teardownListeners();
|
||||||
await fbSignOut(auth);
|
await fbSignOut(auth);
|
||||||
currentUser = null;
|
currentUser = null;
|
||||||
currentUserData = null;
|
currentUserData = null;
|
||||||
|
friendsList = [];
|
||||||
showScreen('auth');
|
showScreen('auth');
|
||||||
showAuthForm();
|
showAuthForm();
|
||||||
}
|
}
|
||||||
@@ -1904,6 +1955,34 @@ document.getElementById('profile-avatar-input').addEventListener('input', update
|
|||||||
document.addEventListener('keydown', e => {
|
document.addEventListener('keydown', e => {
|
||||||
if (e.key === 'Escape') closeProfileModal();
|
if (e.key === 'Escape') closeProfileModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let ptrStartY = 0;
|
||||||
|
let ptrTriggered = false;
|
||||||
|
document.addEventListener('touchstart', e => {
|
||||||
|
if (window.scrollY === 0) ptrStartY = e.touches[0].clientY;
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
document.addEventListener('touchmove', e => {
|
||||||
|
if (!ptrStartY) return;
|
||||||
|
const delta = e.touches[0].clientY - ptrStartY;
|
||||||
|
if (delta > 80 && !ptrTriggered && document.getElementById('screen-dashboard').classList.contains('active')) {
|
||||||
|
ptrTriggered = true;
|
||||||
|
const ind = document.getElementById('ptr-indicator');
|
||||||
|
ind.classList.add('visible');
|
||||||
|
}
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
document.addEventListener('touchend', () => {
|
||||||
|
if (ptrTriggered) {
|
||||||
|
teardownListeners();
|
||||||
|
loadFriends();
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('ptr-indicator').classList.remove('visible');
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
ptrStartY = 0;
|
||||||
|
ptrTriggered = false;
|
||||||
|
}, { passive: true });
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user