buhapp-telegram/public/js/views/chat.js
root 21d281a1c6 Telegram WebApp: Mini App with map, chats, profile, reviews
- index.html with bottom tabbar (Map/Chats/Profile/Settings)
- Telegram WebApp SDK auth via /api/v1/auth/telegram
- Consent gate on first login (4 checkboxes)
- Map view with Leaflet (OSM) + nearby users + radius selector
- Chat list + single chat with WebSocket realtime
- Profile editing (drinks, activities, purposes)
- Review screen with stars + anonymous toggle
- User reviews list with stats
- User profile (public) with rating, write/block/report
- Legal pages (TERMS / PRIVACY / DISCLAIMER)
2026-08-20 17:43:16 +00:00

152 lines
5.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="Сообщение..." 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);
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 {
div.innerHTML = `
<div>${escapeHtml(m.body)}</div>
<div class="meta">
<span>${formatTime(m.created_at)}</span>
${m.edited ? '<span class="edited">ред.</span>' : ''}
</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);
}
} catch {}
};
this.ws.onclose = () => setTimeout(() => this.setupWS(), 3000);
this.ws.onerror = (e) => console.warn('WS error', e);
} catch (e) {
console.warn(e);
}
},
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);
// WS пришлёт обратно
} 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;