Allow removing friends from leaderboard
This commit is contained in:
parent
3d8729c1ae
commit
44134458c1
6 changed files with 64 additions and 4 deletions
15
app/bot.py
15
app/bot.py
|
|
@ -106,6 +106,16 @@ def parse_cancellation(raw: str) -> int:
|
||||||
return transaction_id
|
return transaction_id
|
||||||
|
|
||||||
|
|
||||||
|
def parse_friend_removal(raw: str) -> int:
|
||||||
|
payload = decode_payload(raw)
|
||||||
|
if payload.get("type") != "friend_remove" or payload.get("v") != 1:
|
||||||
|
raise ValueError("Неизвестный формат данных")
|
||||||
|
friend_user_id = payload.get("friend_user_id")
|
||||||
|
if not isinstance(friend_user_id, int) or friend_user_id <= 0:
|
||||||
|
raise ValueError("Некорректный ID друга")
|
||||||
|
return friend_user_id
|
||||||
|
|
||||||
|
|
||||||
class TelegramBotService:
|
class TelegramBotService:
|
||||||
def __init__(self, settings: Settings, database: Database):
|
def __init__(self, settings: Settings, database: Database):
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
|
|
@ -187,6 +197,11 @@ class TelegramBotService:
|
||||||
if not self.database.cancel_transaction(user.id, transaction_id):
|
if not self.database.cancel_transaction(user.id, transaction_id):
|
||||||
raise ValueError("Запись не найдена или уже отменена")
|
raise ValueError("Запись не найдена или уже отменена")
|
||||||
response = "Запись отменена и больше не участвует в расчётах."
|
response = "Запись отменена и больше не участвует в расчётах."
|
||||||
|
elif action == "friend_remove":
|
||||||
|
friend_user_id = parse_friend_removal(raw)
|
||||||
|
if not self.database.remove_friendship(user.id, friend_user_id):
|
||||||
|
raise ValueError("Пользователь не найден в списке друзей")
|
||||||
|
response = "Пользователь удалён из друзей."
|
||||||
else:
|
else:
|
||||||
raise ValueError("Неизвестный формат данных")
|
raise ValueError("Неизвестный формат данных")
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
|
|
|
||||||
12
app/db.py
12
app/db.py
|
|
@ -298,6 +298,18 @@ class Database:
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [int(row["friend_id"]) for row in rows]
|
return [int(row["friend_id"]) for row in rows]
|
||||||
|
|
||||||
|
def remove_friendship(self, first_user_id: int, second_user_id: int) -> bool:
|
||||||
|
if first_user_id == second_user_id:
|
||||||
|
return False
|
||||||
|
low, high = sorted((first_user_id, second_user_id))
|
||||||
|
with closing(self.connect()) as connection:
|
||||||
|
cursor = connection.execute(
|
||||||
|
"DELETE FROM friendships WHERE user_id_low = ? AND user_id_high = ?",
|
||||||
|
(low, high),
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
def set_meta(self, key: str, value: str) -> None:
|
def set_meta(self, key: str, value: str) -> None:
|
||||||
with closing(self.connect()) as connection:
|
with closing(self.connect()) as connection:
|
||||||
connection.execute(
|
connection.execute(
|
||||||
|
|
|
||||||
|
|
@ -449,13 +449,36 @@
|
||||||
? `<img class="leader-avatar" src="..${item.avatar_url}" alt="" loading="lazy" />`
|
? `<img class="leader-avatar" src="..${item.avatar_url}" alt="" loading="lazy" />`
|
||||||
: `<span class="leader-avatar avatar-fallback">${initial}</span>`;
|
: `<span class="leader-avatar avatar-fallback">${initial}</span>`;
|
||||||
const percent = Number(item.saved_percent);
|
const percent = Number(item.saved_percent);
|
||||||
|
const canRemove = state.board === "friends" && item.user_id !== userId;
|
||||||
return `<div class="leader-row ${item.user_id === userId ? "me" : ""}">
|
return `<div class="leader-row ${item.user_id === userId ? "me" : ""}">
|
||||||
<span class="rank">${index + 1}</span>
|
<span class="rank">${index + 1}</span>
|
||||||
${avatar}
|
${avatar}
|
||||||
<span class="leader-name"><strong>${escapeHtml(item.nickname)}</strong><small>${item.user_id === userId ? "Это вы" : money.format(item.balance_cents / 100)}</small></span>
|
<span class="leader-name"><strong>${escapeHtml(item.nickname)}</strong>${item.user_id === userId ? "<small>Это вы</small>" : ""}</span>
|
||||||
<span class="leader-percent ${percent < 0 ? "negative" : ""}">${percent.toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%</span>
|
<span class="leader-actions">
|
||||||
|
<strong class="leader-percent ${percent < 0 ? "negative" : ""}">${percent.toLocaleString("ru-RU", { maximumFractionDigits: 1 })}%</strong>
|
||||||
|
${canRemove ? `<button class="friend-remove" type="button" data-friend-user-id="${item.user_id}">Удалить</button>` : ""}
|
||||||
|
</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
|
list.querySelectorAll(".friend-remove").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => removeFriend(Number(button.dataset.friendUserId)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFriend(friendUserId) {
|
||||||
|
const send = (confirmed) => {
|
||||||
|
if (!confirmed) return;
|
||||||
|
sendTelegramPayload({ v: 1, type: "friend_remove", friend_user_id: friendUserId });
|
||||||
|
};
|
||||||
|
if (tg?.showConfirm && tg.isVersionAtLeast?.("6.2")) {
|
||||||
|
try {
|
||||||
|
tg.showConfirm("Удалить пользователя из друзей?", send);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Telegram confirmation failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
send(window.confirm("Удалить пользователя из друзей?"));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function shareInvite() {
|
async function shareInvite() {
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@
|
||||||
<meta name="color-scheme" content="dark" />
|
<meta name="color-scheme" content="dark" />
|
||||||
<meta name="theme-color" content="#111111" />
|
<meta name="theme-color" content="#111111" />
|
||||||
<title>Пожитки</title>
|
<title>Пожитки</title>
|
||||||
<link rel="stylesheet" href="./styles.css?v=18" />
|
<link rel="stylesheet" href="./styles.css?v=19" />
|
||||||
<script src="./telegram-web-app.js"></script>
|
<script src="./telegram-web-app.js"></script>
|
||||||
<script src="./app.js?v=17" defer></script>
|
<script src="./app.js?v=19" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="shell">
|
<main class="shell">
|
||||||
|
|
|
||||||
|
|
@ -200,8 +200,10 @@ h2 { margin: 0; font-size: 23px; letter-spacing: -.025em; }
|
||||||
.leader-name { min-width: 0; }
|
.leader-name { min-width: 0; }
|
||||||
.leader-name strong { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 13px; }
|
.leader-name strong { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 13px; }
|
||||||
.leader-name small { color: var(--muted); font-size: 10px; }
|
.leader-name small { color: var(--muted); font-size: 10px; }
|
||||||
|
.leader-actions { display: grid; justify-items: end; gap: 4px; }
|
||||||
.leader-percent { font-size: 15px; font-weight: 850; color: var(--accent); }
|
.leader-percent { font-size: 15px; font-weight: 850; color: var(--accent); }
|
||||||
.leader-percent.negative { color: var(--danger); }
|
.leader-percent.negative { color: var(--danger); }
|
||||||
|
.friend-remove { padding: 1px 0; border: 0; background: transparent; color: var(--danger); font-size: 10px; }
|
||||||
.friend-hint { margin: 13px 18px; color: var(--muted); font-size: 11px; line-height: 1.5; text-align: center; }
|
.friend-hint { margin: 13px 18px; color: var(--muted); font-size: 11px; line-height: 1.5; text-align: center; }
|
||||||
|
|
||||||
.bottom-nav { position: fixed; z-index: 10; bottom: max(12px, calc(var(--app-safe-bottom) + 8px)); left: 50%; display: flex; align-items: stretch; gap: 9px; width: min(340px, calc(100% - var(--app-safe-left) - var(--app-safe-right) - 24px)); height: 54px; transform: translateX(-50%); }
|
.bottom-nav { position: fixed; z-index: 10; bottom: max(12px, calc(var(--app-safe-bottom) + 8px)); left: 50%; display: flex; align-items: stretch; gap: 9px; width: min(340px, calc(100% - var(--app-safe-left) - var(--app-safe-right) - 24px)); height: 54px; transform: translateX(-50%); }
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import pytest
|
||||||
from app.bot import (
|
from app.bot import (
|
||||||
parse_cancellation,
|
parse_cancellation,
|
||||||
parse_category,
|
parse_category,
|
||||||
|
parse_friend_removal,
|
||||||
parse_transaction,
|
parse_transaction,
|
||||||
webapp_url_for_user,
|
webapp_url_for_user,
|
||||||
)
|
)
|
||||||
|
|
@ -56,6 +57,13 @@ def test_friendship_is_symmetric_and_idempotent(tmp_path: Path) -> None:
|
||||||
assert not database.add_friendship(3, 7)
|
assert not database.add_friendship(3, 7)
|
||||||
assert database.friend_ids(3) == [7]
|
assert database.friend_ids(3) == [7]
|
||||||
assert database.friend_ids(7) == [3]
|
assert database.friend_ids(7) == [3]
|
||||||
|
assert database.remove_friendship(3, 7)
|
||||||
|
assert database.friend_ids(3) == []
|
||||||
|
assert database.friend_ids(7) == []
|
||||||
|
assert not database.remove_friendship(3, 7)
|
||||||
|
assert parse_friend_removal(
|
||||||
|
'{"v":1,"type":"friend_remove","friend_user_id":7}'
|
||||||
|
) == 7
|
||||||
|
|
||||||
|
|
||||||
def test_filtered_leaderboard(tmp_path: Path) -> None:
|
def test_filtered_leaderboard(tmp_path: Path) -> None:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue