mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
feat(admin-tickets): deep-link to a specific ticket from notifications
Add an /admin/tickets/:ticketId route that pre-selects the ticket, and a StartParamNavigator that maps a Telegram Mini App start param (admin_ticket_<id>, delivered by a group-chat t.me/<bot>/<app>?startapp deep link) to that route. This lets the bot's admin ticket notification buttons open the cabinet straight on the ticket — web_app buttons for private chats, the startapp deep link for groups where web_app is unavailable. Selection mirrors the URL param (cleared on the bare /admin/tickets list). The static /admin/tickets/settings route still out-ranks the new dynamic segment. Pairs with bot feature BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot#2988.
This commit is contained in:
13
src/App.tsx
13
src/App.tsx
@@ -632,6 +632,19 @@ function App() {
|
|||||||
</PermissionRoute>
|
</PermissionRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{/* Deep-link target for admin ticket notification buttons (bot issue #2988):
|
||||||
|
opens a specific ticket directly. Static "/settings" above out-ranks
|
||||||
|
this dynamic segment in react-router, so there is no conflict. */}
|
||||||
|
<Route
|
||||||
|
path="/admin/tickets/:ticketId"
|
||||||
|
element={
|
||||||
|
<PermissionRoute permission="tickets:read">
|
||||||
|
<LazyPage>
|
||||||
|
<AdminTickets />
|
||||||
|
</LazyPage>
|
||||||
|
</PermissionRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/admin/settings"
|
path="/admin/settings"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
hideBackButton,
|
hideBackButton,
|
||||||
onBackButtonClick,
|
onBackButtonClick,
|
||||||
offBackButtonClick,
|
offBackButtonClick,
|
||||||
|
retrieveLaunchParams,
|
||||||
} from '@telegram-apps/sdk-react';
|
} from '@telegram-apps/sdk-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import Twemoji from 'react-twemoji';
|
import Twemoji from 'react-twemoji';
|
||||||
@@ -172,12 +173,51 @@ function TelegramBackButton() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** `admin_ticket_<id>` startapp param → /admin/tickets/<id>. */
|
||||||
|
const ADMIN_TICKET_START_PARAM_RE = /^admin_ticket_(\d+)$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routes a Telegram Mini App start param to an in-app destination on launch.
|
||||||
|
*
|
||||||
|
* Admin ticket notification buttons in GROUP/channel chats open the cabinet via
|
||||||
|
* a `t.me/<bot>/<app>?startapp=admin_ticket_<id>` deep link (bot issue #2988) —
|
||||||
|
* `web_app` buttons don't work in group chats, so the startapp param is the only
|
||||||
|
* way in. Telegram delivers it as `tgWebAppStartParam`; we map it to the admin
|
||||||
|
* ticket route once on mount. Access is still gated by the route's
|
||||||
|
* `PermissionRoute permission="tickets:read"`.
|
||||||
|
*/
|
||||||
|
function StartParamNavigator() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const handled = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (handled.current) return;
|
||||||
|
handled.current = true;
|
||||||
|
|
||||||
|
let startParam: string | undefined;
|
||||||
|
try {
|
||||||
|
startParam = retrieveLaunchParams().tgWebAppStartParam;
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!startParam) return;
|
||||||
|
|
||||||
|
const match = ADMIN_TICKET_START_PARAM_RE.exec(startParam);
|
||||||
|
if (match) {
|
||||||
|
navigate(`/admin/tickets/${match[1]}`, { replace: true });
|
||||||
|
}
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function AppWithNavigator() {
|
export function AppWithNavigator() {
|
||||||
const isTelegram = isInTelegramWebApp();
|
const isTelegram = isInTelegramWebApp();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
{isTelegram && <TelegramBackButton />}
|
{isTelegram && <TelegramBackButton />}
|
||||||
|
{isTelegram && <StartParamNavigator />}
|
||||||
<ErrorBoundary level="page">
|
<ErrorBoundary level="page">
|
||||||
<PlatformProvider>
|
<PlatformProvider>
|
||||||
<ThemeColorsProvider>
|
<ThemeColorsProvider>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from 'react';
|
|||||||
import logger from '../utils/logger';
|
import logger from '../utils/logger';
|
||||||
import { linkifyText } from '../utils/linkify';
|
import { linkifyText } from '../utils/linkify';
|
||||||
import { MessageMediaGrid } from '../components/tickets/MessageMediaGrid';
|
import { MessageMediaGrid } from '../components/tickets/MessageMediaGrid';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate, useParams } from 'react-router';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { adminApi, AdminTicket, AdminTicketDetail } from '../api/admin';
|
import { adminApi, AdminTicket, AdminTicketDetail } from '../api/admin';
|
||||||
@@ -53,10 +53,29 @@ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|||||||
export default function AdminTickets() {
|
export default function AdminTickets() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { ticketId } = useParams<{ ticketId: string }>();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { capabilities } = usePlatform();
|
const { capabilities } = usePlatform();
|
||||||
|
|
||||||
const [selectedTicketId, setSelectedTicketId] = useState<number | null>(null);
|
const [selectedTicketId, setSelectedTicketId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// Deep-link: /admin/tickets/:ticketId (or a startapp param routed here) opens
|
||||||
|
// the given ticket directly — used by the admin-chat notification buttons.
|
||||||
|
// Both routes render the same component instance (no remount), so we mirror the
|
||||||
|
// URL param into the selection: navigating to the bare /admin/tickets list
|
||||||
|
// clears any deep-linked selection, keeping URL and detail pane in sync. (This
|
||||||
|
// only fires on mount or an actual param change, never on in-list clicks, since
|
||||||
|
// ticketId stays undefined on the bare route.)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ticketId) {
|
||||||
|
setSelectedTicketId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = Number(ticketId);
|
||||||
|
if (Number.isInteger(id) && id > 0) {
|
||||||
|
setSelectedTicketId(id);
|
||||||
|
}
|
||||||
|
}, [ticketId]);
|
||||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
const [statusFilter, setStatusFilter] = useState<string>('');
|
||||||
const [replyText, setReplyText] = useState('');
|
const [replyText, setReplyText] = useState('');
|
||||||
const [isReplying, setIsReplying] = useState(false);
|
const [isReplying, setIsReplying] = useState(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user