feat: shop buy-coins CTA, pin chats (max 3), surrender cooldown, OTP hardening
- Shop: when short on coins the detail sheet now shows "{n} more coins" + a
"Get coins" CTA that opens the buy-coins page (was a dead disabled button).
- Chat: pin/unpin conversations (max 3, persisted to localStorage); pinned float
to the top with a gold pin. i18n chat.pin/unpin/pinLimit.
- Surrender: server now rate-limits forfeit asks at a human teammate
(45s per-user cooldown) so it can't be spammed. (Bot teammate still ends
immediately; teammate confirm dialog already existed.)
- OTP login hardening: Kavenegar send now parses the API status from the body
(HTTP 200 can still be a failure) + logs it + 12s timeout; client auth fetch
gets a 20s AbortController timeout so a lost response surfaces an error
instead of freezing on "sending…".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -152,6 +152,9 @@ const fa: Dict = {
|
||||
"chat.placeholder": "پیام بنویسید…",
|
||||
"chat.send": "ارسال",
|
||||
"chat.emoji": "ایموجی",
|
||||
"chat.pin": "سنجاق",
|
||||
"chat.unpin": "برداشتن سنجاق",
|
||||
"chat.pinLimit": "حداکثر {n} گفتگو را میتوانید سنجاق کنید",
|
||||
"chat.typing": "در حال نوشتن…",
|
||||
"city.rewardTitle": "شهرت را انتخاب کن!",
|
||||
"city.rewardSub": "{n} سکه هدیه بگیر",
|
||||
@@ -260,6 +263,8 @@ const fa: Dict = {
|
||||
"shop.title": "فروشگاه",
|
||||
"shop.buy": "خرید",
|
||||
"shop.owned": "موجود",
|
||||
"shop.getCoins": "شارژ سکه",
|
||||
"shop.needMore": "{n} سکه کم دارید",
|
||||
"shop.luxury": "ویژه",
|
||||
"shop.avatars": "آواتارها",
|
||||
"shop.themes": "تمها",
|
||||
@@ -528,6 +533,9 @@ const en: Dict = {
|
||||
"chat.placeholder": "Type a message…",
|
||||
"chat.send": "Send",
|
||||
"chat.emoji": "Emoji",
|
||||
"chat.pin": "Pin",
|
||||
"chat.unpin": "Unpin",
|
||||
"chat.pinLimit": "You can pin up to {n} chats",
|
||||
"chat.typing": "typing…",
|
||||
"city.rewardTitle": "Set your city!",
|
||||
"city.rewardSub": "Earn {n} coins",
|
||||
@@ -633,6 +641,8 @@ const en: Dict = {
|
||||
"shop.title": "Shop",
|
||||
"shop.buy": "Buy",
|
||||
"shop.owned": "Owned",
|
||||
"shop.getCoins": "Get coins",
|
||||
"shop.needMore": "You need {n} more coins",
|
||||
"shop.luxury": "Luxury",
|
||||
"shop.avatars": "Avatars",
|
||||
"shop.themes": "Themes",
|
||||
|
||||
@@ -80,11 +80,21 @@ export class SignalrService implements OnlineService {
|
||||
/* ------------------------------ helpers ---------------------------- */
|
||||
|
||||
private async api<T>(path: string, body: unknown): Promise<T> {
|
||||
const res = await fetch(`${SERVER}${path}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
// Bound the request so a hung/lost response (CDN, network) surfaces an error
|
||||
// instead of freezing the UI (e.g. the OTP "sending…" button forever).
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), 20000);
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`${SERVER}${path}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user