- 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
179 lines
6.3 KiB
JavaScript
179 lines
6.3 KiB
JavaScript
// Single chat view
|
|
const ChatView = {
|
|
chatId: null,
|
|
otherId: null,
|
|
otherName: null,
|
|
ws: null,
|
|
|
|
async render(root, params) {
|
|
this.chatId = params.chatId;
|
|
this.otherId = params.otherId;
|
|
this.otherName = params.otherName;
|
|
root.innerHTML = `
|
|
<div class="view" style="display:flex;flex-direction:column;height:calc(100vh - 100px);padding:0;">
|
|
<div style="padding:12px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;">
|
|
<button class="btn ghost" style="width:auto;margin:0;padding:6px 10px;" id="chat-back">←</button>
|
|
<div class="name" style="flex:1;font-weight:600;">${escapeHtml(this.otherName || '')}</div>
|
|
<button class="btn ghost" style="width:auto;margin:0;padding:6px 10px;" id="chat-profile">👤</button>
|
|
<button class="btn ghost" style="width:auto;margin:0;padding:6px 10px;" id="chat-review">⭐</button>
|
|
</div>
|
|
<div id="messages" style="flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;"></div>
|
|
<div id="edit-bar" style="display:none;padding:6px 12px;background:var(--bg-elev);border-top:1px solid var(--border);font-size:13px;">
|
|
<span>Редактирование</span>
|
|
<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="Сообщение..." autocomplete="off" enterkeyhint="send" style="margin:0;">
|
|
<button class="btn" id="msg-send">➤</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('chat-back').onclick = () => App.showView('chats');
|
|
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();
|
|
},
|
|
|
|
async loadMessages() {
|
|
try {
|
|
const r = await API.ChatApi.listMessages(this.chatId);
|
|
const box = document.getElementById('messages');
|
|
box.innerHTML = '';
|
|
r.messages.forEach((m) => this.appendMessage(m));
|
|
box.scrollTop = box.scrollHeight;
|
|
} catch (e) {
|
|
console.warn(e);
|
|
}
|
|
},
|
|
|
|
appendMessage(m) {
|
|
const box = document.getElementById('messages');
|
|
const me = API.Store.user && m.sender_id === API.Store.user.id;
|
|
const div = document.createElement('div');
|
|
div.className = 'bubble ' + (me ? 'mine' : 'theirs');
|
|
div.dataset.id = m.id;
|
|
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) {
|
|
div.onclick = () => this.onMessageClick(m);
|
|
}
|
|
}
|
|
box.appendChild(div);
|
|
box.scrollTop = box.scrollHeight;
|
|
},
|
|
|
|
onMessageClick(m) {
|
|
TG_APP.showConfirm('Удалить сообщение?').then((ok) => {
|
|
if (ok) this.deleteMessage(m);
|
|
});
|
|
},
|
|
|
|
async deleteMessage(m) {
|
|
try {
|
|
await API.ChatApi.deleteMessage(m.id);
|
|
const el = document.querySelector(`.bubble[data-id="${m.id}"]`);
|
|
if (el) el.innerHTML = '<em>Удалено</em>';
|
|
} catch (e) {
|
|
TG_APP.showAlert('Ошибка: ' + e.message);
|
|
}
|
|
},
|
|
|
|
setupWS() {
|
|
if (!API.Store.access) return;
|
|
const url = (window.API_BASE || 'https://api.buhapp.mygoodservice.ru').replace(/^http/, 'ws') + '/ws?token=' + API.Store.access;
|
|
try {
|
|
this.ws = new WebSocket(url);
|
|
this.ws.onopen = () => console.log('WS connected');
|
|
this.ws.onmessage = (ev) => {
|
|
try {
|
|
const d = JSON.parse(ev.data);
|
|
if (d.type === 'message' && d.payload.chat_id === this.chatId) {
|
|
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 {}
|
|
};
|
|
this.ws.onclose = () => setTimeout(() => this.setupWS(), 3000);
|
|
this.ws.onerror = (e) => console.warn('WS error', e);
|
|
} catch (e) {
|
|
console.warn(e);
|
|
}
|
|
},
|
|
|
|
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) {
|
|
el.querySelector('div:first-child') && (el.querySelector('div:first-child').textContent = p.body);
|
|
const meta = el.querySelector('.meta');
|
|
if (meta && !meta.querySelector('.edited')) {
|
|
const ed = document.createElement('span');
|
|
ed.className = 'edited';
|
|
ed.textContent = 'ред.';
|
|
meta.appendChild(ed);
|
|
}
|
|
}
|
|
},
|
|
|
|
async send() {
|
|
const inp = document.getElementById('msg-input');
|
|
const body = inp.value.trim();
|
|
if (!body) return;
|
|
inp.value = '';
|
|
try {
|
|
await API.ChatApi.sendMessage(this.chatId, body);
|
|
} catch (e) {
|
|
TG_APP.showAlert('Ошибка: ' + e.message);
|
|
}
|
|
},
|
|
|
|
async markRead() {
|
|
try { await API.ChatApi.markRead(this.chatId); } catch {}
|
|
},
|
|
|
|
cleanup() {
|
|
if (this.ws) { this.ws.close(); this.ws = null; }
|
|
},
|
|
};
|
|
|
|
window.ChatView = ChatView;
|