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
This commit is contained in:
root 2026-08-20 20:45:27 +00:00
parent 149f31d25d
commit 7fa2dc83e4
3 changed files with 80 additions and 14 deletions

View File

@ -483,3 +483,20 @@ label { font-size: 13px; color: var(--text-dim); margin-bottom: 4px; display: bl
.onboarding-dot { animation: none !important; transition: none !important; }
.onboarding-slide { transform: none; opacity: 1; pointer-events: auto; }
}
/* Pulse animation для важных кнопок (позвать на действие) */
.btn-pulse {
display: inline-block;
animation: btn-pulse 2s ease-in-out infinite;
}
@keyframes btn-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.2); }
}
/* Read-marks для чата */
.bubble .meta { display: flex; align-items: center; gap: 4px; }
.bubble .read-tick { font-size: 12px; opacity: 0.85; }
.bubble .read-tick.read { color: var(--secondary); font-weight: 700; }
/* edit marker */
.bubble .edited { font-style: italic; opacity: 0.7; }

View File

@ -23,7 +23,7 @@ const ChatView = {
<button id="edit-cancel" style="float:right;background:none;border:none;color:var(--danger);font-weight:600;">Отмена</button>
</div>
<div class="chat-input">
<input id="msg-input" class="input" placeholder="Сообщение..." style="margin:0;">
<input id="msg-input" class="input" placeholder="Сообщение..." autocomplete="off" enterkeyhint="send" style="margin:0;">
<button class="btn" id="msg-send"></button>
</div>
</div>
@ -33,6 +33,14 @@ const ChatView = {
document.getElementById('chat-profile').onclick = () => App.openUserProfile(this.otherId, this.otherName);
document.getElementById('chat-review').onclick = () => App.openReview(this.chatId, this.otherId, this.otherName);
document.getElementById('msg-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.send();
}
});
document.getElementById('msg-send').onclick = () => this.send();
this.loadMessages();
this.setupWS();
this.markRead();
@ -59,11 +67,16 @@ const ChatView = {
if (m.deleted) {
div.innerHTML = '<em>Удалено</em>';
} else {
// ✓✓ если прочитано собеседником, ✓ если только доставлено
const readMark = me
? (m.read_at ? '<span class="read-tick read">✓✓</span>' : '<span class="read-tick">✓</span>')
: '';
div.innerHTML = `
<div>${escapeHtml(m.body)}</div>
<div class="meta">
<span>${formatTime(m.created_at)}</span>
${m.edited ? '<span class="edited">ред.</span>' : ''}
${readMark}
</div>
`;
if (me) {
@ -103,6 +116,8 @@ const ChatView = {
this.appendMessage(d.payload);
} else if (d.type === 'message_edited' && d.payload.chat_id === this.chatId) {
this.updateMessage(d.payload);
} else if (d.type === 'message_read' && d.payload.chat_id === this.chatId) {
this.markMessagesRead();
}
} catch {}
};
@ -113,6 +128,18 @@ const ChatView = {
}
},
async markMessagesRead() {
try {
const r = await API.ChatApi.listMessages(this.chatId);
r.messages.forEach((m) => {
if (m.read_at) {
const el = document.querySelector(`.bubble[data-id="${m.id}"] .read-tick`);
if (el) { el.classList.add('read'); el.textContent = '✓✓'; }
}
});
} catch {}
},
updateMessage(p) {
const el = document.querySelector(`.bubble[data-id="${p.id}"]`);
if (el) {
@ -134,7 +161,6 @@ const ChatView = {
inp.value = '';
try {
await API.ChatApi.sendMessage(this.chatId, body);
// WS пришлёт обратно
} catch (e) {
TG_APP.showAlert('Ошибка: ' + e.message);
}

View File

@ -21,7 +21,9 @@ const MapView = {
<span class="slider"></span>
</label>
</div>
<button class="btn" id="loc-share-btn" style="margin-top:8px;">📍 Поделиться геолокацией</button>
<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>
@ -52,17 +54,21 @@ const MapView = {
this.visible = e.target.checked;
try {
await API.Auth.setVisibility(this.visible);
this.refreshLocation();
if (window._lastKnownPos) this.applyPosition(window._lastKnownPos);
} catch (err) {
API.Store && console.warn(err);
console.warn(err);
TG_APP.showAlert('Не удалось обновить видимость');
}
});
document.getElementById('radius-sel').addEventListener('change', (e) => {
document.getElementById('radius-sel').addEventListener('change', async (e) => {
this.radius = parseInt(e.target.value, 10);
document.getElementById('radius-label').textContent = this.radius + ' км';
this.refreshNearby();
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) => {
@ -73,6 +79,10 @@ const MapView = {
// Если уже есть сохранённые координаты — покажем на карте
if (window._lastKnownPos) {
this.applyPosition(window._lastKnownPos);
} else {
// Авто-запрос геолокации при первом заходе на карту
// Не подвешиваем UI — requestLocation вернётся мгновенно, а браузер сам спросит у пользователя
this.requestLocation();
}
},
@ -90,13 +100,24 @@ const MapView = {
navigator.geolocation.getCurrentPosition(
async (pos) => {
window._lastKnownPos = pos;
if (btn) { btn.disabled = false; btn.textContent = '📍 Обновить геолокацию'; }
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.textContent = '📍 Попробовать снова'; }
if (status) status.textContent = '⚠️ ' + this.geolocationErrorText(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 }
@ -105,7 +126,7 @@ const MapView = {
geolocationErrorText(err) {
switch (err.code) {
case err.PERMISSION_DENIED: return 'Доступ к геолокации запрещён. Разреши в настройках браузера.';
case err.PERMISSION_DENIED: return 'Доступ к геолокации запрещён.';
case err.POSITION_UNAVAILABLE: return 'Не удалось определить координаты (нет GPS/Wi-Fi).';
case err.TIMEOUT: return 'Таймаут запроса геолокации.';
default: return 'Неизвестная ошибка геолокации.';
@ -164,6 +185,8 @@ const MapView = {
this.markers.push(m);
});
} catch (e) {
const c = document.getElementById('nearby-count');
if (c) c.textContent = '📍 нужна геолокация';
console.warn(e);
}
},