feat: add admin telegram profile links and avatar preview
This commit is contained in:
@@ -15,6 +15,10 @@ def setup_admin_routes(app: web.Application) -> None:
|
|||||||
router.add_post(
|
router.add_post(
|
||||||
"/api/admin/users/{user_id:-?\\d+}/message/preview", admin_user_message_preview_route
|
"/api/admin/users/{user_id:-?\\d+}/message/preview", admin_user_message_preview_route
|
||||||
)
|
)
|
||||||
|
router.add_post(
|
||||||
|
"/api/admin/users/{user_id:-?\\d+}/telegram-profile-link",
|
||||||
|
admin_user_telegram_profile_link_route,
|
||||||
|
)
|
||||||
router.add_post("/api/admin/users/{user_id:-?\\d+}/reset-trial", admin_user_reset_trial_route)
|
router.add_post("/api/admin/users/{user_id:-?\\d+}/reset-trial", admin_user_reset_trial_route)
|
||||||
router.add_post("/api/admin/users/{user_id:-?\\d+}/extend", admin_user_extend_route)
|
router.add_post("/api/admin/users/{user_id:-?\\d+}/extend", admin_user_extend_route)
|
||||||
router.add_post(
|
router.add_post(
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
# ruff: noqa: F401,F403,F405,I001
|
# ruff: noqa: F401,F403,F405,I001
|
||||||
from ._runtime import * # noqa: F403,F405
|
from ._runtime import * # noqa: F403,F405
|
||||||
|
|
||||||
|
from html import escape as html_escape
|
||||||
|
|
||||||
|
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
|
|
||||||
|
|
||||||
async def admin_users_list_route(request: web.Request) -> web.Response:
|
async def admin_users_list_route(request: web.Request) -> web.Response:
|
||||||
_require_admin_user_id(request)
|
_require_admin_user_id(request)
|
||||||
@@ -624,6 +628,107 @@ async def admin_user_message_preview_route(request: web.Request) -> web.Response
|
|||||||
return _ok({})
|
return _ok({})
|
||||||
|
|
||||||
|
|
||||||
|
def _admin_user_display_name_for_message(user: User) -> str:
|
||||||
|
full = " ".join(
|
||||||
|
part
|
||||||
|
for part in [getattr(user, "first_name", None), getattr(user, "last_name", None)]
|
||||||
|
if part
|
||||||
|
).strip()
|
||||||
|
return (
|
||||||
|
full
|
||||||
|
or (f"@{user.username}" if getattr(user, "username", None) else None)
|
||||||
|
or getattr(user, "email", None)
|
||||||
|
or f"User #{user.user_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def admin_user_telegram_profile_link_route(request: web.Request) -> web.Response:
|
||||||
|
actor_id = _require_admin_user_id(request)
|
||||||
|
admin_telegram_id = request.get("admin_telegram_id")
|
||||||
|
if not admin_telegram_id:
|
||||||
|
return _error(403, "admin_telegram_unavailable")
|
||||||
|
|
||||||
|
queue_manager = get_queue_manager()
|
||||||
|
if not queue_manager:
|
||||||
|
return _error(503, "queue_unavailable")
|
||||||
|
|
||||||
|
target_id = int(request.match_info["user_id"])
|
||||||
|
settings: Settings = request.app["settings"]
|
||||||
|
async_session_factory: sessionmaker = request.app["async_session_factory"]
|
||||||
|
|
||||||
|
async with async_session_factory() as session:
|
||||||
|
target_user = await user_dal.get_user_by_id(session, target_id)
|
||||||
|
if not target_user:
|
||||||
|
return _error(404, "not_found")
|
||||||
|
if not target_user.telegram_id:
|
||||||
|
return _error(404, "no_telegram_account")
|
||||||
|
|
||||||
|
admin_user = await user_dal.get_user_by_id(session, actor_id)
|
||||||
|
lang = (
|
||||||
|
getattr(admin_user, "language_code", None)
|
||||||
|
or getattr(settings, "DEFAULT_LANGUAGE", None)
|
||||||
|
or "ru"
|
||||||
|
)
|
||||||
|
|
||||||
|
await message_log_dal.create_message_log(
|
||||||
|
session,
|
||||||
|
{
|
||||||
|
"user_id": actor_id,
|
||||||
|
"event_type": "admin_profile_link_webapp",
|
||||||
|
"content": f"Requested Telegram profile link for user_id={target_id}",
|
||||||
|
"is_admin_event": True,
|
||||||
|
"target_user_id": target_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
i18n_instance = request.app.get("i18n")
|
||||||
|
translate = (
|
||||||
|
(lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs))
|
||||||
|
if i18n_instance is not None
|
||||||
|
else (lambda key, **kwargs: key.format(**kwargs) if kwargs else key)
|
||||||
|
)
|
||||||
|
target_name = _admin_user_display_name_for_message(target_user)
|
||||||
|
telegram_id = int(target_user.telegram_id)
|
||||||
|
profile_url = f"tg://user?id={telegram_id}"
|
||||||
|
message_text = translate(
|
||||||
|
"admin_user_profile_link_message",
|
||||||
|
name=html_escape(target_name),
|
||||||
|
user_id=target_user.user_id,
|
||||||
|
telegram_id=telegram_id,
|
||||||
|
)
|
||||||
|
if message_text == "admin_user_profile_link_message":
|
||||||
|
message_text = (
|
||||||
|
f"Профиль пользователя: <b>{html_escape(target_name)}</b>\n"
|
||||||
|
f"User ID: <code>{target_user.user_id}</code>\n"
|
||||||
|
f"Telegram ID: <code>{telegram_id}</code>\n\n"
|
||||||
|
"Нажмите кнопку ниже, чтобы открыть профиль в Telegram."
|
||||||
|
)
|
||||||
|
|
||||||
|
button_text = translate("user_card_open_profile_button")
|
||||||
|
if button_text == "user_card_open_profile_button":
|
||||||
|
button_text = "👤 Открыть профиль"
|
||||||
|
|
||||||
|
markup = InlineKeyboardMarkup(
|
||||||
|
inline_keyboard=[[InlineKeyboardButton(text=button_text, url=profile_url)]]
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await send_message_via_queue(
|
||||||
|
queue_manager,
|
||||||
|
int(admin_telegram_id),
|
||||||
|
MessageContent(content_type="text", text=message_text),
|
||||||
|
parse_mode="HTML",
|
||||||
|
disable_web_page_preview=True,
|
||||||
|
reply_markup=markup,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Admin profile link message enqueue failed: %s", exc)
|
||||||
|
return _error(502, "send_failed", str(exc))
|
||||||
|
|
||||||
|
return _ok({"queued": True})
|
||||||
|
|
||||||
|
|
||||||
async def admin_user_delete_route(request: web.Request) -> web.Response:
|
async def admin_user_delete_route(request: web.Request) -> web.Response:
|
||||||
actor_id = _require_admin_user_id(request)
|
actor_id = _require_admin_user_id(request)
|
||||||
target_id = int(request.match_info["user_id"])
|
target_id = int(request.match_info["user_id"])
|
||||||
|
|||||||
@@ -59,10 +59,13 @@
|
|||||||
} from "../lib/admin/format.js";
|
} from "../lib/admin/format.js";
|
||||||
import {
|
import {
|
||||||
createGravatarCache,
|
createGravatarCache,
|
||||||
|
openTelegramProfileLink,
|
||||||
userAvatarUrl,
|
userAvatarUrl,
|
||||||
userDisplayName,
|
userDisplayName,
|
||||||
userInitials,
|
userInitials,
|
||||||
userSecondaryName,
|
userSecondaryName,
|
||||||
|
userTelegramProfileLink,
|
||||||
|
userTelegramProfileLinkKind,
|
||||||
} from "../lib/admin/users.js";
|
} from "../lib/admin/users.js";
|
||||||
|
|
||||||
export let api;
|
export let api;
|
||||||
@@ -679,6 +682,9 @@
|
|||||||
{userDisplayName}
|
{userDisplayName}
|
||||||
{userSecondaryName}
|
{userSecondaryName}
|
||||||
{userInitials}
|
{userInitials}
|
||||||
|
{userTelegramProfileLink}
|
||||||
|
{userTelegramProfileLinkKind}
|
||||||
|
{openTelegramProfileLink}
|
||||||
{paymentStatusVariant}
|
{paymentStatusVariant}
|
||||||
{trafficPercentValue}
|
{trafficPercentValue}
|
||||||
{trafficLeftLabel}
|
{trafficLeftLabel}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
import {
|
import {
|
||||||
Copy,
|
Copy,
|
||||||
Eye,
|
Eye,
|
||||||
|
ExternalLink,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Send,
|
Send,
|
||||||
Plus,
|
Plus,
|
||||||
@@ -36,6 +37,13 @@
|
|||||||
export let trafficOfLabel;
|
export let trafficOfLabel;
|
||||||
export let userInitials = () => "";
|
export let userInitials = () => "";
|
||||||
export let fmtDateShort = (v) => v;
|
export let fmtDateShort = (v) => v;
|
||||||
|
export let userTelegramProfileLink = () => "";
|
||||||
|
export let userTelegramProfileLinkKind = () => "";
|
||||||
|
export let openTelegramProfileLink = () => false;
|
||||||
|
|
||||||
|
let avatarPreviewOpen = false;
|
||||||
|
let avatarPreviewUrl = "";
|
||||||
|
let avatarPreviewName = "";
|
||||||
|
|
||||||
function pretty(val) {
|
function pretty(val) {
|
||||||
if (val === true) return at("yes", {}, "Да");
|
if (val === true) return at("yes", {}, "Да");
|
||||||
@@ -67,9 +75,53 @@
|
|||||||
$: userLogsHasMore =
|
$: userLogsHasMore =
|
||||||
Number(userLogsTotal || 0) > (Number(userLogsPage || 0) + 1) * Number(userLogsPageSize || 20);
|
Number(userLogsTotal || 0) > (Number(userLogsPage || 0) + 1) * Number(userLogsPageSize || 20);
|
||||||
|
|
||||||
|
$: openedUserAvatarUrl = openedUser ? resolvedAvatarUrl(openedUser) : "";
|
||||||
|
$: openedUserTelegramProfileLink = openedUser ? userTelegramProfileLink(openedUser) : "";
|
||||||
|
$: openedUserTelegramProfileLinkKind = openedUser ? userTelegramProfileLinkKind(openedUser) : "";
|
||||||
|
$: openedUserTelegramProfileHint =
|
||||||
|
openedUserTelegramProfileLinkKind === "id"
|
||||||
|
? at(
|
||||||
|
"user_open_tg_profile_id_hint",
|
||||||
|
{},
|
||||||
|
"Профиль будет открыт по Telegram ID. Telegram может заблокировать переход из-за настроек приватности пользователя или ограничений клиента."
|
||||||
|
)
|
||||||
|
: at("user_open_tg_profile_hint", {}, "Открыть профиль Telegram");
|
||||||
|
|
||||||
$: if (openedUser && userDetailTab === "logs" && !userLogsLoading && !userLogsLoaded) {
|
$: if (openedUser && userDetailTab === "logs" && !userLogsLoading && !userLogsLoaded) {
|
||||||
usersStore.loadUserLogs(0);
|
usersStore.loadUserLogs(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: if (!openedUser) {
|
||||||
|
avatarPreviewOpen = false;
|
||||||
|
avatarPreviewUrl = "";
|
||||||
|
avatarPreviewName = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAvatarPreview() {
|
||||||
|
if (!openedUserAvatarUrl || !openedUser) return;
|
||||||
|
avatarPreviewUrl = openedUserAvatarUrl;
|
||||||
|
avatarPreviewName = userDisplayName(openedUser);
|
||||||
|
avatarPreviewOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAvatarPreview() {
|
||||||
|
avatarPreviewOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openUserTelegramProfile() {
|
||||||
|
if (!openedUserTelegramProfileLink) {
|
||||||
|
usersStore.copyToClipboard(
|
||||||
|
String(openedUser?.telegram_id || ""),
|
||||||
|
at("user_tg_profile_unavailable", {}, "Ссылка на профиль Telegram недоступна")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (openedUserTelegramProfileLinkKind === "id") {
|
||||||
|
usersStore.sendTelegramProfileLink();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openTelegramProfileLink(openedUserTelegramProfileLink);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -89,18 +141,21 @@
|
|||||||
<div class="admin-user-dialog-body">
|
<div class="admin-user-dialog-body">
|
||||||
<aside class="admin-user-aside">
|
<aside class="admin-user-aside">
|
||||||
<div class="admin-user-summary">
|
<div class="admin-user-summary">
|
||||||
<span class="admin-avatar admin-avatar-lg">
|
<button
|
||||||
{#if resolvedAvatarUrl(openedUser)}
|
type="button"
|
||||||
<img
|
class="admin-avatar admin-avatar-lg admin-avatar-preview-trigger"
|
||||||
src={resolvedAvatarUrl(openedUser)}
|
class:is-clickable={Boolean(openedUserAvatarUrl)}
|
||||||
alt=""
|
disabled={!openedUserAvatarUrl}
|
||||||
loading="lazy"
|
onclick={openAvatarPreview}
|
||||||
referrerpolicy="no-referrer"
|
aria-label={at("user_avatar_open", {}, "Открыть аватар")}
|
||||||
/>
|
title={openedUserAvatarUrl ? at("user_avatar_open", {}, "Открыть аватар") : ""}
|
||||||
|
>
|
||||||
|
{#if openedUserAvatarUrl}
|
||||||
|
<img src={openedUserAvatarUrl} alt="" loading="lazy" referrerpolicy="no-referrer" />
|
||||||
{:else}
|
{:else}
|
||||||
<span>{userInitials(openedUser)}</span>
|
<span>{userInitials(openedUser)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</button>
|
||||||
<div class="admin-user-summary-meta">
|
<div class="admin-user-summary-meta">
|
||||||
<strong>{userDisplayName(openedUser)}</strong>
|
<strong>{userDisplayName(openedUser)}</strong>
|
||||||
<small>{userSecondaryName(openedUser)}</small>
|
<small>{userSecondaryName(openedUser)}</small>
|
||||||
@@ -120,6 +175,24 @@
|
|||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="admin-user-summary-actions">
|
||||||
|
<AdminButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onclick={openUserTelegramProfile}
|
||||||
|
disabled={!openedUserTelegramProfileLink}
|
||||||
|
title={openedUserTelegramProfileHint}
|
||||||
|
aria-label={at("user_open_tg_profile", {}, "Открыть профиль Telegram")}
|
||||||
|
>
|
||||||
|
<ExternalLink size={14} />
|
||||||
|
{at("user_open_tg_profile", {}, "Открыть Telegram")}
|
||||||
|
</AdminButton>
|
||||||
|
</div>
|
||||||
|
{#if openedUserTelegramProfileLinkKind === "id"}
|
||||||
|
<small class="admin-user-telegram-profile-note"
|
||||||
|
>{openedUserTelegramProfileHint}</small
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -871,6 +944,25 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={avatarPreviewOpen}
|
||||||
|
title={avatarPreviewName || at("user_avatar_title", {}, "Аватар")}
|
||||||
|
closeLabel={at("close", {}, "Закрыть")}
|
||||||
|
onclose={closeAvatarPreview}
|
||||||
|
class="admin-dialog admin-avatar-dialog"
|
||||||
|
>
|
||||||
|
{#if avatarPreviewUrl}
|
||||||
|
<div class="admin-avatar-preview">
|
||||||
|
<img
|
||||||
|
src={avatarPreviewUrl}
|
||||||
|
alt={avatarPreviewName}
|
||||||
|
loading="eager"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={userMessageConfirmOpen}
|
open={userMessageConfirmOpen}
|
||||||
title={at("user_msg_confirm_title", {}, "Отправить сообщение пользователю?")}
|
title={at("user_msg_confirm_title", {}, "Отправить сообщение пользователю?")}
|
||||||
@@ -1054,6 +1146,78 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
.admin-avatar-preview-trigger {
|
||||||
|
padding: 0;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
.admin-avatar-preview-trigger img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.admin-avatar-preview-trigger:disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.admin-avatar-preview-trigger.is-clickable {
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
.admin-avatar-preview-trigger.is-clickable:hover,
|
||||||
|
.admin-avatar-preview-trigger.is-clickable:focus-visible {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
|
||||||
|
}
|
||||||
|
.admin-user-summary-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.admin-user-telegram-profile-note {
|
||||||
|
display: block;
|
||||||
|
margin-top: 2px;
|
||||||
|
color: var(--admin-dim);
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
:global(.admin-avatar-dialog) {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
|
width: min(920px, calc(100vw - 28px));
|
||||||
|
height: min(820px, calc(100dvh - 28px));
|
||||||
|
max-height: calc(100dvh - 28px);
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.admin-avatar-preview {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.admin-avatar-preview img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid var(--admin-border);
|
||||||
|
background: var(--admin-surface-2);
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
:global(.dialog:has(.admin-avatar-dialog)) {
|
||||||
|
padding: max(8px, env(safe-area-inset-top)) max(8px, env(safe-area-inset-right))
|
||||||
|
max(8px, env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left));
|
||||||
|
}
|
||||||
|
:global(.admin-avatar-dialog) {
|
||||||
|
width: calc(100vw - 16px);
|
||||||
|
height: min(88dvh, calc(100dvh - 16px));
|
||||||
|
max-height: calc(100dvh - 16px);
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
:global(.admin-user-dialog .admin-user-logs-tab) {
|
:global(.admin-user-dialog .admin-user-logs-tab) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -319,6 +319,28 @@ export function createUsersStore({ api, onToast, at }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendTelegramProfileLink() {
|
||||||
|
let s;
|
||||||
|
state.update((st) => {
|
||||||
|
s = st;
|
||||||
|
return st;
|
||||||
|
});
|
||||||
|
if (!s.openedUser) return;
|
||||||
|
state.update((st) => ({ ...st, userActionBusy: true }));
|
||||||
|
try {
|
||||||
|
const res = await api(`/admin/users/${s.openedUser.user_id}/telegram-profile-link`, {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
if (res?.ok) {
|
||||||
|
onToast(at("user_tg_profile_link_sent", {}, "Ссылка отправлена в Telegram"));
|
||||||
|
} else {
|
||||||
|
onToast(res?.error || at("user_tg_profile_link_failed", {}, "Не удалось отправить ссылку"));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
state.update((st) => ({ ...st, userActionBusy: false }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function extendUser() {
|
async function extendUser() {
|
||||||
let s;
|
let s;
|
||||||
state.update((st) => {
|
state.update((st) => {
|
||||||
@@ -510,6 +532,7 @@ export function createUsersStore({ api, onToast, at }) {
|
|||||||
sendUserMessage,
|
sendUserMessage,
|
||||||
requestSendUserMessage,
|
requestSendUserMessage,
|
||||||
previewUserMessage,
|
previewUserMessage,
|
||||||
|
sendTelegramProfileLink,
|
||||||
extendUser,
|
extendUser,
|
||||||
resetTrialUser,
|
resetTrialUser,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
|
|||||||
@@ -25,6 +25,50 @@ export function userAvatarUrl(user) {
|
|||||||
return value && !value.startsWith("/api/account/avatar") ? value : "";
|
return value && !value.startsWith("/api/account/avatar") ? value : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function userTelegramProfileLink(user) {
|
||||||
|
const username = String(user?.username || "")
|
||||||
|
.trim()
|
||||||
|
.replace(/^@+/, "");
|
||||||
|
if (username) return `https://t.me/${encodeURIComponent(username)}`;
|
||||||
|
|
||||||
|
const telegramId = Number(user?.telegram_id);
|
||||||
|
if (Number.isFinite(telegramId) && telegramId > 0) {
|
||||||
|
return `tg://user?id=${encodeURIComponent(String(Math.trunc(telegramId)))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userTelegramProfileLinkKind(user) {
|
||||||
|
return String(user?.username || "").trim()
|
||||||
|
? "username"
|
||||||
|
: userTelegramProfileLink(user)
|
||||||
|
? "id"
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openTelegramProfileLink(link) {
|
||||||
|
if (!link || typeof window === "undefined") return false;
|
||||||
|
|
||||||
|
const tg = window.Telegram?.WebApp;
|
||||||
|
if (/^https:\/\/(?:t|telegram)\.me\//i.test(link) && typeof tg?.openTelegramLink === "function") {
|
||||||
|
try {
|
||||||
|
tg.openTelegramLink(link);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
// Fall through to the normal browser/deep-link handling below.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^https?:\/\//i.test(link)) {
|
||||||
|
window.open(link, "_blank", "noopener,noreferrer");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = link;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export function createGravatarCache(onResolved = () => {}) {
|
export function createGravatarCache(onResolved = () => {}) {
|
||||||
const cache = new Map();
|
const cache = new Map();
|
||||||
const pending = new Map();
|
const pending = new Map();
|
||||||
|
|||||||
@@ -1057,6 +1057,15 @@
|
|||||||
"admin_status_disabled": "Disabled",
|
"admin_status_disabled": "Disabled",
|
||||||
"admin_status_bot_only": "Bot Only",
|
"admin_status_bot_only": "Bot Only",
|
||||||
"admin_user_detail_title": "User #{id}",
|
"admin_user_detail_title": "User #{id}",
|
||||||
|
"admin_user_avatar_open": "Open avatar",
|
||||||
|
"admin_user_avatar_title": "Avatar",
|
||||||
|
"admin_user_open_tg_profile": "Open Telegram",
|
||||||
|
"admin_user_open_tg_profile_hint": "Open Telegram profile",
|
||||||
|
"admin_user_open_tg_profile_id_hint": "This user has no username: the bot will send you a Telegram message with a profile button.",
|
||||||
|
"admin_user_tg_profile_unavailable": "Telegram profile link is unavailable",
|
||||||
|
"admin_user_tg_profile_link_sent": "Link sent to Telegram",
|
||||||
|
"admin_user_tg_profile_link_failed": "Failed to send link",
|
||||||
|
"admin_user_profile_link_message": "User profile: <b>{name}</b>\nUser ID: <code>{user_id}</code>\nTelegram ID: <code>{telegram_id}</code>\n\nTap the button below to open the profile in Telegram.",
|
||||||
"admin_close": "Close",
|
"admin_close": "Close",
|
||||||
"admin_loading": "Loading…",
|
"admin_loading": "Loading…",
|
||||||
"admin_badge_banned": "Banned",
|
"admin_badge_banned": "Banned",
|
||||||
|
|||||||
@@ -1057,6 +1057,15 @@
|
|||||||
"admin_status_disabled": "Выключен",
|
"admin_status_disabled": "Выключен",
|
||||||
"admin_status_bot_only": "Только бот",
|
"admin_status_bot_only": "Только бот",
|
||||||
"admin_user_detail_title": "Пользователь #{id}",
|
"admin_user_detail_title": "Пользователь #{id}",
|
||||||
|
"admin_user_avatar_open": "Открыть аватар",
|
||||||
|
"admin_user_avatar_title": "Аватар",
|
||||||
|
"admin_user_open_tg_profile": "Открыть Telegram",
|
||||||
|
"admin_user_open_tg_profile_hint": "Открыть профиль Telegram",
|
||||||
|
"admin_user_open_tg_profile_id_hint": "Пользователь без username: бот отправит вам в Telegram сообщение с кнопкой открытия профиля.",
|
||||||
|
"admin_user_tg_profile_unavailable": "Ссылка на профиль Telegram недоступна",
|
||||||
|
"admin_user_tg_profile_link_sent": "Ссылка отправлена в Telegram",
|
||||||
|
"admin_user_tg_profile_link_failed": "Не удалось отправить ссылку",
|
||||||
|
"admin_user_profile_link_message": "Профиль пользователя: <b>{name}</b>\nUser ID: <code>{user_id}</code>\nTelegram ID: <code>{telegram_id}</code>\n\nНажмите кнопку ниже, чтобы открыть профиль в Telegram.",
|
||||||
"admin_close": "Закрыть",
|
"admin_close": "Закрыть",
|
||||||
"admin_loading": "Загрузка…",
|
"admin_loading": "Загрузка…",
|
||||||
"admin_badge_banned": "Бан",
|
"admin_badge_banned": "Бан",
|
||||||
|
|||||||
Reference in New Issue
Block a user