feat: nickname system + friend requests + privacy settings
- 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 <nick> sends request, /friends shows list + requests - Client: full friends UI with requests, accept/decline, nickname setup
This commit is contained in:
parent
3733b4b94f
commit
f14b61d7d9
175
game.js
175
game.js
|
|
@ -220,7 +220,14 @@ function customConfirm(msg, onYes) {
|
||||||
btnInvite.textContent = '📤 Пригласить друзей';
|
btnInvite.textContent = '📤 Пригласить друзей';
|
||||||
btnInvite.onclick = () => {
|
btnInvite.onclick = () => {
|
||||||
const code = worldId || generateShortWorldCode();
|
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);
|
box.appendChild(btnInvite);
|
||||||
|
|
||||||
|
|
@ -234,69 +241,145 @@ function customConfirm(msg, onYes) {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(SERVER_URL + '/api/tg/friends?tg_id=' + tgUser.id);
|
const resp = await fetch(SERVER_URL + '/api/tg/friends?tg_id=' + tgUser.id);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (!data.ok || !data.friends || !data.friends.length) {
|
|
||||||
showToast('👥 Нет друзей. Поделись своим TG ID: ' + tgUser.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let html = '<div style="text-align:left;margin-bottom:16px;">';
|
|
||||||
for (const f of data.friends) {
|
|
||||||
if (f.online) {
|
|
||||||
html += '<div style="padding:8px 0;border-bottom:1px solid #333;">🟢 <b>' + f.name + '</b> — мир ' + f.world_id + '</div>';
|
|
||||||
} else {
|
|
||||||
html += '<div style="padding:8px 0;border-bottom:1px solid #333;">⚫ ' + f.name + '</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
box.innerHTML = '';
|
box.innerHTML = '';
|
||||||
const ft = document.createElement('div');
|
const ft = document.createElement('div');
|
||||||
ft.style.cssText = 'font-size:20px;font-weight:bold;margin-bottom:8px;color:#f39c12;';
|
ft.style.cssText = 'font-size:20px;font-weight:bold;margin-bottom:8px;color:#f39c12;';
|
||||||
ft.textContent = '👥 Друзья';
|
ft.textContent = '👥 Друзья';
|
||||||
box.appendChild(ft);
|
box.appendChild(ft);
|
||||||
|
// 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 = '<div style="color:#e74c3c;font-size:14px;margin-bottom:8px;">Задай ник, чтобы друзья могли тебя найти:</div>';
|
||||||
|
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');
|
const list = document.createElement('div');
|
||||||
list.innerHTML = html;
|
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 + ' <b>' + f.nickname + '</b>' + (f.world_id ? ' — мир ' + f.world_id : '');
|
||||||
|
fDiv.innerHTML = '<div>' + info + '</div>';
|
||||||
|
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);
|
box.appendChild(list);
|
||||||
// Add friend by TG ID
|
} 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 = '<div style="color:#f39c12;font-weight:bold;margin-bottom:6px;">📨 Запросы в друзья:</div>';
|
||||||
|
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');
|
const addDiv = document.createElement('div');
|
||||||
addDiv.style.cssText = 'margin-top:16px;';
|
addDiv.style.cssText = 'margin-top:12px;';
|
||||||
addDiv.innerHTML = '<div style="color:#888;font-size:14px;margin-bottom:8px;">Добавить друга по TG ID:</div>';
|
addDiv.innerHTML = '<div style="color:#888;font-size:14px;margin-bottom:8px;">Добавить друга по нику:</div>';
|
||||||
const addInput = document.createElement('input');
|
const addInput = document.createElement('input');
|
||||||
addInput.type = 'number';
|
addInput.type = 'text';
|
||||||
addInput.placeholder = 'TG ID друга';
|
addInput.maxLength = 16;
|
||||||
addInput.style.cssText = 'width:70%;padding:10px;border:2px solid #f39c12;border-radius:8px;background:#16213e;color:#fff;font-size:16px;';
|
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');
|
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.textContent = '+';
|
||||||
addBtn.onclick = async () => {
|
addBtn.onclick = async () => {
|
||||||
const fid = parseInt(addInput.value);
|
const nick = addInput.value.trim().toLowerCase();
|
||||||
if (!fid) return;
|
if (!nick || nick.length < 2) { showToast('❌ Минимум 2 символа'); 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 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();
|
const d = await r.json();
|
||||||
if (d.ok) { showToast('✅ Друг добавлен!'); btnFriends.click(); }
|
if (d.ok) { showToast(d.message === 'Already friends' ? '✅ Уже друзья!' : '📨 Запрос отправлен!'); }
|
||||||
else { showToast('❌ Ошибка'); }
|
else { showToast('❌ ' + (d.error || 'Ошибка')); }
|
||||||
};
|
};
|
||||||
addDiv.appendChild(addInput);
|
addDiv.appendChild(addInput);
|
||||||
addDiv.appendChild(addBtn);
|
addDiv.appendChild(addBtn);
|
||||||
box.appendChild(addDiv);
|
box.appendChild(addDiv);
|
||||||
// My TG ID
|
// Privacy settings
|
||||||
const myId = document.createElement('div');
|
const settings = data.settings || { allow_requests: 1, notify_online: 1 };
|
||||||
myId.style.cssText = 'margin-top:12px;color:#888;font-size:13px;';
|
const settingsDiv = document.createElement('div');
|
||||||
myId.textContent = '🆔 Твой ID: ' + tgUser.id + ' — отправь другу, чтобы он добавил тебя';
|
settingsDiv.style.cssText = 'margin-top:12px;display:flex;gap:12px;font-size:13px;color:#888;';
|
||||||
box.appendChild(myId);
|
const reqLabel = document.createElement('label');
|
||||||
// Join online friend buttons
|
reqLabel.style.cssText = 'cursor:pointer;';
|
||||||
const onlineFriends = (data.friends||[]).filter(f => f.online);
|
const reqCb = document.createElement('input');
|
||||||
if (onlineFriends.length) {
|
reqCb.type = 'checkbox';
|
||||||
for (const of2 of onlineFriends) {
|
reqCb.checked = settings.allow_requests;
|
||||||
const joinBtn = document.createElement('button');
|
reqCb.style.cssText = 'margin-right:4px;';
|
||||||
joinBtn.style.cssText = 'width:100%;padding:12px;margin-top:8px;background:#27ae60;color:#fff;border:none;border-radius:10px;font-size:16px;cursor:pointer;';
|
reqCb.onchange = async () => {
|
||||||
joinBtn.textContent = '🎮 Вступить к ' + of2.name + ' (мир ' + of2.world_id + ')';
|
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})});
|
||||||
joinBtn.onclick = () => {
|
|
||||||
worldId = of2.world_id;
|
|
||||||
overlay.remove();
|
|
||||||
resolve(worldId);
|
|
||||||
};
|
};
|
||||||
box.appendChild(joinBtn);
|
reqLabel.appendChild(reqCb);
|
||||||
}
|
reqLabel.appendChild(document.createTextNode(' Запросы в друзья'));
|
||||||
}
|
settingsDiv.appendChild(reqLabel);
|
||||||
|
box.appendChild(settingsDiv);
|
||||||
// Back button
|
// Back button
|
||||||
const btnBackF = document.createElement('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;';
|
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;';
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="game.js?v=48"></script>
|
<script src="game.js?v=50"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue