diff --git a/package-lock.json b/package-lock.json
index 00d27fd9ee..1528872f92 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -45,6 +45,7 @@
"supercluster": "^8.0.1",
"telegram": "^2.26.22",
"topojson-client": "^3.1.0",
+ "uqr": "^0.1.2",
"ws": "^8.19.0",
"youtubei.js": "^16.0.1"
},
@@ -27327,8 +27328,7 @@
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.2.tgz",
"integrity": "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
diff --git a/package.json b/package.json
index 52a5d2508a..2f41e3cc83 100644
--- a/package.json
+++ b/package.json
@@ -110,9 +110,9 @@
"fast-xml-parser": "^5.3.7",
"globe.gl": "^2.45.0",
"hls.js": "^1.6.15",
- "jose": "^6.0.11",
"i18next": "^25.8.10",
"i18next-browser-languagedetector": "^8.2.1",
+ "jose": "^6.0.11",
"maplibre-gl": "^5.16.0",
"marked": "^17.0.3",
"onnxruntime-web": "^1.23.2",
@@ -123,6 +123,7 @@
"supercluster": "^8.0.1",
"telegram": "^2.26.22",
"topojson-client": "^3.1.0",
+ "uqr": "^0.1.2",
"ws": "^8.19.0",
"youtubei.js": "^16.0.1"
},
diff --git a/src/services/preferences-content.ts b/src/services/preferences-content.ts
index b89e4c7e77..4f9eed21ca 100644
--- a/src/services/preferences-content.ts
+++ b/src/services/preferences-content.ts
@@ -7,6 +7,7 @@ import type { StreamQuality } from '@/services/ai-flow-settings';
import { getThemePreference, setThemePreference, type ThemePreference } from '@/utils/theme-manager';
import { getFontFamily, setFontFamily, type FontFamily } from '@/services/font-settings';
import { escapeHtml } from '@/utils/sanitize';
+import { renderSVG } from 'uqr';
import { trackLanguageChange } from '@/services/analytics';
import { exportSettings, importSettings, type ImportResult } from '@/utils/settings-persistence';
import {
@@ -808,23 +809,57 @@ export function renderPreferences(host: PreferencesHost): PreferencesResult {
container.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
- if (target.closest('#usConnectTelegram')) {
- const rowEl = target.closest('.us-notif-ch-row') as HTMLElement | null;
- if (!rowEl) return;
+ if (target.closest('.us-notif-tg-copy-btn')) {
+ const btn = target.closest('.us-notif-tg-copy-btn') as HTMLButtonElement;
+ const cmd = btn.dataset.cmd ?? '';
+ const markCopied = () => {
+ btn.textContent = 'Copied!';
+ setTimeout(() => { btn.textContent = 'Copy'; }, 2000);
+ };
+ const execFallback = () => {
+ const ta = document.createElement('textarea');
+ ta.value = cmd;
+ ta.style.cssText = 'position:fixed;opacity:0;pointer-events:none';
+ document.body.appendChild(ta);
+ ta.select();
+ try { document.execCommand('copy'); markCopied(); } catch { /* ignore */ }
+ document.body.removeChild(ta);
+ };
+ if (navigator.clipboard?.writeText) {
+ navigator.clipboard.writeText(cmd).then(markCopied).catch(execFallback);
+ } else {
+ execFallback();
+ }
+ return;
+ }
+
+ const startTelegramPairing = (rowEl: HTMLElement) => {
+ rowEl.innerHTML = `
${channelIcon('telegram')}
`;
createPairingToken().then(({ token, expiresAt }) => {
if (signal.aborted) return;
const botUsername = (typeof import.meta !== 'undefined' && import.meta.env?.VITE_TELEGRAM_BOT_USERNAME as string | undefined) ?? 'WorldMonitorBot';
- const deepLink = `https://t.me/${botUsername}?start=${token}`;
+ const deepLink = `https://t.me/${String(botUsername)}?start=${token}`;
+ const startCmd = `/start ${token}`;
const secsLeft = Math.max(0, Math.floor((expiresAt - Date.now()) / 1000));
+ const qrSvg = renderSVG(deepLink, { ecc: 'M', border: 1 });
rowEl.innerHTML = `
${channelIcon('telegram')}
-
Telegram
-
Waiting for pairing...
+
Connect Telegram
+
Open the bot. If Telegram doesn't send the code automatically, paste this command.
+
`;
let remaining = secsLeft;
@@ -833,20 +868,39 @@ export function renderPreferences(host: PreferencesHost): PreferencesResult {
if (signal.aborted) { clearNotifPoll(); return; }
remaining -= 3;
const countdownEl = container.querySelector('#usTgCountdown');
- if (countdownEl) countdownEl.textContent = `${Math.max(0, remaining)}s`;
+ if (countdownEl) countdownEl.textContent = `Waiting… ${Math.max(0, remaining)}s`;
const expired = remaining <= 0;
- if (expired) clearNotifPoll();
+ if (expired) {
+ clearNotifPoll();
+ rowEl.innerHTML = `
+ ${channelIcon('telegram')}
+
+
Telegram
+
Code expired
+
+
+
+
+ `;
+ return;
+ }
getChannelsData().then((data) => {
const tg = data.channels.find(c => c.channelType === 'telegram');
- if (tg?.verified || expired) {
- if (tg?.verified) saveRuleWithNewChannel('telegram');
+ if (tg?.verified) {
+ saveRuleWithNewChannel('telegram');
reloadNotifSection();
}
- }).catch(() => {
- if (expired) reloadNotifSection();
- });
+ }).catch(() => {});
}, 3000);
- }).catch(() => {});
+ }).catch(() => {
+ rowEl.innerHTML = `${channelIcon('telegram')}
Telegram
Failed to generate code
`;
+ });
+ };
+
+ if (target.closest('#usConnectTelegram') || target.closest('.us-notif-tg-regen')) {
+ const rowEl = target.closest('.us-notif-ch-row') as HTMLElement | null;
+ if (!rowEl) return;
+ startTelegramPairing(rowEl);
return;
}
diff --git a/src/styles/settings-window.css b/src/styles/settings-window.css
index 9fc8726432..4b36d0d2e6 100644
--- a/src/styles/settings-window.css
+++ b/src/styles/settings-window.css
@@ -1255,9 +1255,97 @@ tr.diag-err td { color: var(--settings-red); }
background: color-mix(in srgb, var(--settings-accent) 85%, white);
}
+.us-notif-tg-inline-link {
+ color: var(--settings-accent);
+ text-decoration: none;
+}
+
+.us-notif-tg-inline-link:hover {
+ text-decoration: underline;
+}
+
+.us-notif-tg-pair-layout {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ margin-top: 6px;
+}
+
+.us-notif-tg-qr {
+ flex-shrink: 0;
+ width: 88px;
+ height: 88px;
+ border-radius: 6px;
+ overflow: hidden;
+ background: #fff;
+ padding: 3px;
+ box-sizing: border-box;
+}
+
+.us-notif-tg-qr svg {
+ width: 100%;
+ height: 100%;
+ display: block;
+}
+
+.us-notif-tg-cmd-col {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ justify-content: center;
+ min-width: 0;
+}
+
+.us-notif-tg-cmd-row {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.us-notif-tg-cmd {
+ font-family: monospace;
+ font-size: 11px;
+ background: var(--settings-bg-secondary, rgba(0,0,0,0.15));
+ border: 1px solid var(--settings-border, rgba(255,255,255,0.1));
+ border-radius: 4px;
+ padding: 2px 6px;
+ color: var(--settings-text-primary);
+ user-select: all;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 180px;
+}
+
+.us-notif-tg-copy-btn {
+ font: inherit;
+ font-size: 10px;
+ font-weight: 600;
+ padding: 2px 8px;
+ background: transparent;
+ border: 1px solid var(--settings-accent);
+ border-radius: 4px;
+ color: var(--settings-accent);
+ cursor: pointer;
+ white-space: nowrap;
+ transition: background 0.15s, color 0.15s;
+}
+
+.us-notif-tg-copy-btn:hover {
+ background: var(--settings-accent);
+ color: #fff;
+}
+
+.us-notif-tg-expired {
+ color: var(--settings-text-secondary);
+ font-style: italic;
+}
+
.us-notif-tg-countdown {
font-size: 10px;
color: var(--settings-text-secondary);
+ white-space: nowrap;
}
.us-notif-signin {