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:
parent
149f31d25d
commit
7fa2dc83e4
@ -482,4 +482,21 @@ label { font-size: 13px; color: var(--text-dim); margin-bottom: 4px; display: bl
|
||||
.onboarding-icon,
|
||||
.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; }
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -149,4 +175,4 @@ const ChatView = {
|
||||
},
|
||||
};
|
||||
|
||||
window.ChatView = ChatView;
|
||||
window.ChatView = ChatView;
|
||||
|
||||
@ -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,9 +185,11 @@ const MapView = {
|
||||
this.markers.push(m);
|
||||
});
|
||||
} catch (e) {
|
||||
const c = document.getElementById('nearby-count');
|
||||
if (c) c.textContent = '📍 нужна геолокация';
|
||||
console.warn(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
window.MapView = MapView;
|
||||
window.MapView = MapView;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user