From f14b61d7d952cd249d251e9a048d29430eecc911 Mon Sep 17 00:00:00 2001 From: Mk Date: Wed, 27 May 2026 04:30:52 +0000 Subject: [PATCH] feat: nickname system + friend requests + privacy settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nicknames: unique, 2-16 chars, alphanumeric + russian + underscore - Friend requests: send by nickname, accept/decline with confirmation - No raw TG IDs exposed — only nicknames - Privacy: allow_requests toggle, notify_online toggle - Bot: /code shows nickname, /add sends request, /friends shows list + requests - Client: full friends UI with requests, accept/decline, nickname setup --- game.js | 181 ++++++++++++++++++++++++++++++++++++++--------------- index.html | 2 +- 2 files changed, 133 insertions(+), 50 deletions(-) diff --git a/game.js b/game.js index bab6ed4..40a3483 100644 --- a/game.js +++ b/game.js @@ -220,7 +220,14 @@ function customConfirm(msg, onYes) { btnInvite.textContent = '📤 Пригласить друзей'; btnInvite.onclick = () => { const code = worldId || generateShortWorldCode(); - Telegram.WebApp.switchInlineQuery('play ' + code); + // Share: open TG share dialog with game link + const url = 'https://t.me/Grechkacraft_bot/app?startapp=' + encodeURIComponent(code); + if (Telegram.WebApp && Telegram.WebApp.openTelegramLink) { + Telegram.WebApp.openTelegramLink('https://t.me/share/url?url=' + encodeURIComponent(url) + '&text=' + encodeURIComponent('🏗️ Заходи в GrechkaCraft! Мир: ' + code)); + } else { + // Fallback: copy invite link + navigator.clipboard.writeText(url).then(() => showToast('📋 Скопировано!')).catch(() => showToast(url)); + } }; box.appendChild(btnInvite); @@ -234,69 +241,145 @@ function customConfirm(msg, onYes) { try { const resp = await fetch(SERVER_URL + '/api/tg/friends?tg_id=' + tgUser.id); const data = await resp.json(); - if (!data.ok || !data.friends || !data.friends.length) { - showToast('👥 Нет друзей. Поделись своим TG ID: ' + tgUser.id); - return; - } - let html = '
'; - for (const f of data.friends) { - if (f.online) { - html += '
🟢 ' + f.name + ' — мир ' + f.world_id + '
'; - } else { - html += '
⚫ ' + f.name + '
'; - } - } - html += '
'; box.innerHTML = ''; const ft = document.createElement('div'); ft.style.cssText = 'font-size:20px;font-weight:bold;margin-bottom:8px;color:#f39c12;'; ft.textContent = '👥 Друзья'; box.appendChild(ft); - const list = document.createElement('div'); - list.innerHTML = html; - box.appendChild(list); - // Add friend by TG ID + // Show nickname + if (data.nickname) { + const nickDiv = document.createElement('div'); + nickDiv.style.cssText = 'color:#2ecc71;font-size:14px;margin-bottom:12px;'; + nickDiv.textContent = '🔗 Твой ник: ' + data.nickname; + box.appendChild(nickDiv); + } else { + // No nickname yet — set one + const nickDiv = document.createElement('div'); + nickDiv.style.cssText = 'margin-bottom:12px;'; + nickDiv.innerHTML = '
Задай ник, чтобы друзья могли тебя найти:
'; + const nickInput = document.createElement('input'); + nickInput.type = 'text'; + nickInput.maxLength = 16; + nickInput.placeholder = 'Твой ник'; + nickInput.style.cssText = 'width:65%;padding:10px;border:2px solid #f39c12;border-radius:8px;background:#16213e;color:#fff;font-size:16px;'; + const nickBtn = document.createElement('button'); + nickBtn.style.cssText = 'width:28%;padding:10px;background:#f39c12;color:#fff;border:none;border-radius:8px;font-size:16px;cursor:pointer;margin-left:4px;'; + nickBtn.textContent = '✓'; + nickBtn.onclick = async () => { + const n = nickInput.value.trim().toLowerCase(); + if (!n || n.length < 2) { showToast('❌ Минимум 2 символа'); return; } + const check = await fetch(SERVER_URL + '/api/nick/check?nick=' + encodeURIComponent(n)); + const cd = await check.json(); + if (!cd.available) { showToast('❌ Ник занят'); return; } + const set = await fetch(SERVER_URL + '/api/nick/set', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,nickname:n})}); + const sd = await set.json(); + if (sd.ok) { showToast('✅ Ник: ' + sd.nickname); btnFriends.onclick(); } else { showToast('❌ Ошибка'); } + }; + nickDiv.appendChild(nickInput); + nickDiv.appendChild(nickBtn); + box.appendChild(nickDiv); + } + // Friends list + if (data.friends && data.friends.length) { + const list = document.createElement('div'); + list.style.cssText = 'margin-bottom:12px;'; + for (const f of data.friends) { + const fDiv = document.createElement('div'); + fDiv.style.cssText = 'padding:6px 0;border-bottom:1px solid #333;display:flex;justify-content:space-between;align-items:center;'; + const statusDot = f.online ? '🟢' : '⚫'; + const info = statusDot + ' ' + f.nickname + '' + (f.world_id ? ' — мир ' + f.world_id : ''); + fDiv.innerHTML = '
' + info + '
'; + if (f.online && f.world_id) { + const joinBtn = document.createElement('button'); + joinBtn.style.cssText = 'padding:4px 10px;background:#27ae60;color:#fff;border:none;border-radius:6px;font-size:12px;cursor:pointer;'; + joinBtn.textContent = '🎮 Вступить'; + joinBtn.onclick = () => { worldId = f.world_id; overlay.remove(); resolve(worldId); }; + fDiv.appendChild(joinBtn); + } + list.appendChild(fDiv); + } + box.appendChild(list); + } else if (data.friends && !data.friends.length) { + const noFriends = document.createElement('div'); + noFriends.style.cssText = 'color:#888;font-size:14px;margin-bottom:12px;'; + noFriends.textContent = 'Пока нет друзей'; + box.appendChild(noFriends); + } + // Pending friend requests + if (data.pending && data.pending.length) { + const pDiv = document.createElement('div'); + pDiv.style.cssText = 'margin-bottom:12px;'; + pDiv.innerHTML = '
📨 Запросы в друзья:
'; + for (const p of data.pending) { + const pItem = document.createElement('div'); + pItem.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:4px 0;border-bottom:1px solid #222;'; + const span = document.createElement('span'); + span.textContent = p.from; + pItem.appendChild(span); + const btns = document.createElement('div'); + const acceptBtn = document.createElement('button'); + acceptBtn.style.cssText = 'padding:4px 8px;background:#27ae60;color:#fff;border:none;border-radius:4px;font-size:12px;cursor:pointer;margin-right:4px;'; + acceptBtn.textContent = '✓'; + acceptBtn.onclick = async () => { + const r = await fetch(SERVER_URL + '/api/friends/accept', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,request_id:p.id})}); + const d = await r.json(); + if (d.ok) { showToast('✅ Друг добавлен!'); btnFriends.onclick(); } else { showToast('❌ Ошибка'); } + }; + const declineBtn = document.createElement('button'); + declineBtn.style.cssText = 'padding:4px 8px;background:#e74c3c;color:#fff;border:none;border-radius:4px;font-size:12px;cursor:pointer;'; + declineBtn.textContent = '✕'; + declineBtn.onclick = async () => { + await fetch(SERVER_URL + '/api/friends/decline', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,request_id:p.id})}); + showToast('Отклонено'); + btnFriends.onclick(); + }; + btns.appendChild(acceptBtn); + btns.appendChild(declineBtn); + pItem.appendChild(btns); + pDiv.appendChild(pItem); + } + box.appendChild(pDiv); + } + // Add friend by nickname const addDiv = document.createElement('div'); - addDiv.style.cssText = 'margin-top:16px;'; - addDiv.innerHTML = '
Добавить друга по TG ID:
'; + addDiv.style.cssText = 'margin-top:12px;'; + addDiv.innerHTML = '
Добавить друга по нику:
'; const addInput = document.createElement('input'); - addInput.type = 'number'; - addInput.placeholder = 'TG ID друга'; - addInput.style.cssText = 'width:70%;padding:10px;border:2px solid #f39c12;border-radius:8px;background:#16213e;color:#fff;font-size:16px;'; + addInput.type = 'text'; + addInput.maxLength = 16; + addInput.placeholder = 'Ник друга'; + addInput.style.cssText = 'width:65%;padding:10px;border:2px solid #f39c12;border-radius:8px;background:#16213e;color:#fff;font-size:16px;'; const addBtn = document.createElement('button'); - addBtn.style.cssText = 'width:25%;padding:10px;background:#f39c12;color:#fff;border:none;border-radius:8px;font-size:16px;cursor:pointer;margin-left:4px;'; + addBtn.style.cssText = 'width:28%;padding:10px;background:#3498db;color:#fff;border:none;border-radius:8px;font-size:16px;cursor:pointer;margin-left:4px;'; addBtn.textContent = '+'; addBtn.onclick = async () => { - const fid = parseInt(addInput.value); - if (!fid) return; - const r = await fetch(SERVER_URL + '/api/tg/friends/add', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,friend_id:fid})}); + const nick = addInput.value.trim().toLowerCase(); + if (!nick || nick.length < 2) { showToast('❌ Минимум 2 символа'); return; } + const r = await fetch(SERVER_URL + '/api/friends/request', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,nickname:nick})}); const d = await r.json(); - if (d.ok) { showToast('✅ Друг добавлен!'); btnFriends.click(); } - else { showToast('❌ Ошибка'); } + if (d.ok) { showToast(d.message === 'Already friends' ? '✅ Уже друзья!' : '📨 Запрос отправлен!'); } + else { showToast('❌ ' + (d.error || 'Ошибка')); } }; addDiv.appendChild(addInput); addDiv.appendChild(addBtn); box.appendChild(addDiv); - // My TG ID - const myId = document.createElement('div'); - myId.style.cssText = 'margin-top:12px;color:#888;font-size:13px;'; - myId.textContent = '🆔 Твой ID: ' + tgUser.id + ' — отправь другу, чтобы он добавил тебя'; - box.appendChild(myId); - // Join online friend buttons - const onlineFriends = (data.friends||[]).filter(f => f.online); - if (onlineFriends.length) { - for (const of2 of onlineFriends) { - const joinBtn = document.createElement('button'); - joinBtn.style.cssText = 'width:100%;padding:12px;margin-top:8px;background:#27ae60;color:#fff;border:none;border-radius:10px;font-size:16px;cursor:pointer;'; - joinBtn.textContent = '🎮 Вступить к ' + of2.name + ' (мир ' + of2.world_id + ')'; - joinBtn.onclick = () => { - worldId = of2.world_id; - overlay.remove(); - resolve(worldId); - }; - box.appendChild(joinBtn); - } - } + // Privacy settings + const settings = data.settings || { allow_requests: 1, notify_online: 1 }; + const settingsDiv = document.createElement('div'); + settingsDiv.style.cssText = 'margin-top:12px;display:flex;gap:12px;font-size:13px;color:#888;'; + const reqLabel = document.createElement('label'); + reqLabel.style.cssText = 'cursor:pointer;'; + const reqCb = document.createElement('input'); + reqCb.type = 'checkbox'; + reqCb.checked = settings.allow_requests; + reqCb.style.cssText = 'margin-right:4px;'; + reqCb.onchange = async () => { + await fetch(SERVER_URL + '/api/friends/settings', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,allow_requests:reqCb.checked?1:0,notify_online:settings.notify_online})}); + }; + reqLabel.appendChild(reqCb); + reqLabel.appendChild(document.createTextNode(' Запросы в друзья')); + settingsDiv.appendChild(reqLabel); + box.appendChild(settingsDiv); // Back button const btnBackF = document.createElement('button'); btnBackF.style.cssText = 'width:100%;padding:10px;margin-top:12px;background:transparent;color:#888;border:1px solid #444;border-radius:8px;cursor:pointer;font-size:14px;'; diff --git a/index.html b/index.html index 09875a8..0c3b89d 100644 --- a/index.html +++ b/index.html @@ -95,6 +95,6 @@ - +