buhapp-telegram/public/js/views/map.js
root 7fa2dc83e4 UX fixes: auto geolocation, read-ticks, pulse animation
- Map: auto-request geolocation on view enter (no manual click needed)
- Map: pulse animation on 'Share location' button (calls attention)
- Map: better error message on permission denied (with link to settings)
- Map: 'loading...' state on radius change
- Chat: ✓ / ✓✓ read marks on own messages (from m.read_at)
- Chat: WS handler for 'message_read' event (real-time update)
- Chat: enterkeyhint='send' on input (mobile keyboard send button)
- Chat: send on Enter key (no shift)
- CSS: btn-pulse animation
- CSS: read-tick styles
2026-08-20 20:45:27 +00:00

196 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Map view — главный экран
const MapView = {
map: null,
markers: [],
radius: 5,
visible: true,
nearby: [],
async render(root) {
root.innerHTML = `
<div class="view">
<h1>🗺️ Карта</h1>
<div class="card" id="my-info">
<div class="row">
<div>
<strong>Я на карте</strong>
<div class="privacy-note">Другие пользователи видят твоё местоположение ±300м</div>
</div>
<label class="switch">
<input type="checkbox" id="visible-toggle" ${this.visible ? 'checked' : ''}>
<span class="slider"></span>
</label>
</div>
<button class="btn" id="loc-share-btn" style="margin-top:8px;">
<span class="btn-pulse">📍</span> Поделиться геолокацией
</button>
<div class="privacy-note" id="loc-status">Нажми чтобы получить координаты</div>
</div>
<div id="map" style="height:400px;border-radius:12px;overflow:hidden;margin-bottom:12px;"></div>
<div class="row">
<label>Радиус: <strong id="radius-label">${this.radius} км</strong></label>
<select id="radius-sel" style="background:var(--bg-elev);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px;">
<option value="1">1 км</option>
<option value="5" selected>5 км</option>
<option value="10">10 км</option>
<option value="25">25 км</option>
<option value="50">50 км</option>
</select>
</div>
<div id="nearby-count" class="sub">—</div>
</div>
`;
if (!this.map) {
this.map = L.map('map').setView([55.7558, 37.6173], 12);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap',
maxZoom: 19,
}).addTo(this.map);
}
setTimeout(() => this.map.invalidateSize(), 100);
document.getElementById('visible-toggle').addEventListener('change', async (e) => {
this.visible = e.target.checked;
try {
await API.Auth.setVisibility(this.visible);
if (window._lastKnownPos) this.applyPosition(window._lastKnownPos);
} catch (err) {
console.warn(err);
TG_APP.showAlert('Не удалось обновить видимость');
}
});
document.getElementById('radius-sel').addEventListener('change', async (e) => {
this.radius = parseInt(e.target.value, 10);
document.getElementById('radius-label').textContent = this.radius + ' км';
const c = document.getElementById('nearby-count');
if (c) c.textContent = '⏳ поиск...';
if (window._lastKnownPos) {
await this.refreshNearbyFromPos(window._lastKnownPos.coords.latitude, window._lastKnownPos.coords.longitude);
}
});
document.getElementById('loc-share-btn').onclick = (e) => {
e.preventDefault();
this.requestLocation();
};
// Если уже есть сохранённые координаты — покажем на карте
if (window._lastKnownPos) {
this.applyPosition(window._lastKnownPos);
} else {
// Авто-запрос геолокации при первом заходе на карту
// Не подвешиваем UI — requestLocation вернётся мгновенно, а браузер сам спросит у пользователя
this.requestLocation();
}
},
requestLocation() {
if (!navigator.geolocation) {
const s = document.getElementById('loc-status');
if (s) s.textContent = 'Геолокация недоступна в браузере';
return;
}
const btn = document.getElementById('loc-share-btn');
const status = document.getElementById('loc-status');
if (btn) { btn.disabled = true; btn.textContent = '⏳ Запрашиваю…'; }
if (status) status.textContent = 'Браузер спросит разрешение…';
navigator.geolocation.getCurrentPosition(
async (pos) => {
window._lastKnownPos = pos;
if (btn) {
btn.disabled = false;
btn.innerHTML = '<span class="btn-pulse">📍</span> Обновить геолокацию';
}
if (status) status.textContent = `${pos.coords.latitude.toFixed(4)}, ${pos.coords.longitude.toFixed(4)}`;
await this.applyPosition(pos);
},
(err) => {
if (btn) {
btn.disabled = false;
btn.innerHTML = '<span class="btn-pulse">📍</span> Попробовать снова';
}
if (status) {
const msg = this.geolocationErrorText(err);
status.innerHTML = '⚠️ ' + msg + (err.code === 1
? ' <br><small>Открой Настройки → Сайты → Геолокация → buhapp.mygoodservice.ru</small>'
: '');
}
console.warn('geolocation error:', err);
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 60000 }
);
},
geolocationErrorText(err) {
switch (err.code) {
case err.PERMISSION_DENIED: return 'Доступ к геолокации запрещён.';
case err.POSITION_UNAVAILABLE: return 'Не удалось определить координаты (нет GPS/Wi-Fi).';
case err.TIMEOUT: return 'Таймаут запроса геолокации.';
default: return 'Неизвестная ошибка геолокации.';
}
},
async applyPosition(pos) {
const lat = pos.coords.latitude;
const lng = pos.coords.longitude;
try {
await API.Auth.updateLocation(lat, lng, this.visible);
} catch (e) {
console.warn('updateLocation:', e);
}
if (this.map) {
this.map.setView([lat, lng], 13);
// удалим старый круг
this.map.eachLayer((layer) => {
if (layer instanceof L.Circle) this.map.removeLayer(layer);
});
L.circle([lat, lng], {
radius: this.radius * 1000,
color: '#FF6B35', fillColor: '#FF6B35', fillOpacity: 0.1, weight: 2,
}).addTo(this.map);
this.refreshNearbyFromPos(lat, lng);
}
},
async refreshNearby() {
if (!navigator.geolocation) return;
navigator.geolocation.getCurrentPosition(async (pos) => {
this.refreshNearbyFromPos(pos.coords.latitude, pos.coords.longitude);
});
},
async refreshNearbyFromPos(lat, lng) {
try {
const r = await API.Auth.searchNearby(lat, lng, this.radius);
this.nearby = r.results || [];
const c = document.getElementById('nearby-count');
if (c) c.textContent = this.nearby.length + ' чел. рядом';
// удалить старые маркеры (кроме своего круга)
this.markers.forEach(m => this.map.removeLayer(m));
this.markers = [];
this.nearby.forEach((n) => {
const icon = L.divIcon({
className: 'custom-marker',
html: `<div style="width:36px;height:36px;border-radius:50%;background:#FF6B35;color:white;display:flex;align-items:center;justify-content:center;font-weight:700;border:2px solid white;">${escapeHtml((n.name || '?')[0])}</div>`,
iconSize: [36, 36],
});
const m = L.marker([n.lat, n.lng], { icon }).addTo(this.map);
m.bindTooltip(n.name + ' • ' + (n.distance_m / 1000).toFixed(1) + ' км', { direction: 'top' });
m.on('click', () => App.openUserProfile(n.user_id, n.name));
this.markers.push(m);
});
} catch (e) {
const c = document.getElementById('nearby-count');
if (c) c.textContent = '📍 нужна геолокация';
console.warn(e);
}
},
};
window.MapView = MapView;