Add WeChat typing indicators to the DM channel

This commit is contained in:
Coffee
2026-03-26 12:09:07 +08:00
parent 60aaecd684
commit 7dfc5ddd0d
15 changed files with 505 additions and 51 deletions
+35 -2
View File
@@ -596,7 +596,7 @@ pub async fn start_server(
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; \
font-src https://fonts.gstatic.com; \
connect-src 'self'; \
img-src 'self' data:; \
img-src 'self' data: https://liteapp.weixin.qq.com; \
object-src 'none'; \
frame-ancestors 'none'; \
base-uri 'self'; \
@@ -3010,6 +3010,35 @@ mod tests {
Ok(())
}
#[test]
fn test_wechat_active_channel_does_not_require_pairing_status() -> Result<(), String> {
let ext = InstalledExtension {
name: "wechat".to_string(),
kind: ExtensionKind::WasmChannel,
display_name: Some("WeChat".to_string()),
description: None,
url: None,
authenticated: true,
active: true,
tools: Vec::new(),
needs_setup: true,
has_auth: false,
installed: true,
activation_error: None,
version: None,
};
let status = classify_wasm_channel_activation(&ext, false, false);
if status != Some(ExtensionActivationStatus::Active) {
return Err(format!(
"wechat should be active after QR login, got {:?}",
status
));
}
Ok(())
}
#[test]
fn test_channel_relay_activation_status_is_preserved() -> Result<(), String> {
let relay = InstalledExtension {
@@ -3242,7 +3271,7 @@ mod tests {
crate::extensions::InteractiveLoginStartResult {
session_id: "wechat-session-42".to_string(),
status: "pending".to_string(),
message: "Scan the QR code in WeChat to finish connecting.".to_string(),
message: "Open the WeChat QR page to continue.".to_string(),
qr_code_url: Some("https://qr.example/42".to_string()),
instructions: Some(
"Keep this window open while you scan and confirm on your phone."
@@ -3605,6 +3634,10 @@ mod tests {
csp_str.contains("object-src 'none'"),
"CSP must contain object-src 'none'"
);
assert!(
csp_str.contains("img-src 'self' data: https://liteapp.weixin.qq.com"),
"CSP must allow WeChat QR images from liteapp.weixin.qq.com"
);
assert!(
csp_str.contains("frame-ancestors 'none'"),
"CSP must contain frame-ancestors 'none'"
+10 -21
View File
@@ -3251,25 +3251,18 @@ function renderInteractiveLoginPanel() {
const title = document.createElement('div');
title.className = 'configure-verification-title';
title.textContent = 'WeChat QR Login';
title.textContent = 'Open WeChat QR Page';
panel.appendChild(title);
const status = document.createElement('div');
status.className = 'configure-verification-instructions';
status.textContent = 'Generate a QR code to connect this channel.';
status.textContent = 'The QR flow opens in a separate tab.';
status.dataset.qrStatus = 'true';
panel.appendChild(status);
const img = document.createElement('img');
img.className = 'configure-qr-image';
img.alt = 'WeChat QR code';
img.style.display = 'none';
img.dataset.qrImage = 'true';
panel.appendChild(img);
const link = document.createElement('a');
link.className = 'configure-verification-link';
link.textContent = 'Open QR code in a new tab';
link.textContent = 'Open QR Page';
link.target = '_blank';
link.rel = 'noreferrer noopener';
link.style.display = 'none';
@@ -3291,17 +3284,13 @@ function updateInteractiveLoginPanel(overlay, res) {
const panel = getInteractiveLoginPanel(overlay);
if (!panel) return;
const status = panel.querySelector('[data-qr-status="true"]');
const img = panel.querySelector('[data-qr-image="true"]');
const link = panel.querySelector('[data-qr-link="true"]');
panel.style.display = '';
if (status) {
status.textContent = res.message || '';
}
if (img && res.qr_code_url) {
img.src = res.qr_code_url;
img.style.display = '';
status.textContent = res.status === 'refreshed'
? 'The QR page was refreshed. Open it again if needed.'
: 'The QR flow opens in a separate tab.';
}
if (link && res.qr_code_url) {
@@ -3322,7 +3311,7 @@ function setInteractiveLoginBusy(overlay, busy, label) {
function startInteractiveLogin(name, overlay) {
if (!overlay || !document.body.contains(overlay)) return;
clearConfigureInlineError(overlay);
setConfigureInlineStatus(overlay, 'Generating WeChat QR code...');
setConfigureInlineStatus(overlay, 'Preparing WeChat QR page...');
setInteractiveLoginBusy(overlay, true, 'Waiting for scan...');
apiFetch('/api/extensions/' + encodeURIComponent(name) + '/login/start', {
@@ -3703,9 +3692,9 @@ function renderWasmChannelStepper(ext) {
var status = ext.activation_status || 'installed';
var steps = [
{ label: 'Installed', key: 'installed' },
{ label: 'Configured', key: 'configured' },
{ label: status === 'pairing' ? 'Awaiting Pairing' : 'Active', key: 'active' },
{ label: I18n.t('status.installed'), key: 'installed' },
{ label: I18n.t('status.configured'), key: 'configured' },
{ label: status === 'pairing' ? I18n.t('status.pairingShort') : I18n.t('status.active'), key: 'active' },
];
var reachedIdx;
+2
View File
@@ -54,7 +54,9 @@ I18n.register('en', {
'status.restart': 'Restart',
'status.active': 'Active',
'status.installed': 'Installed',
'status.configured': 'Configured',
'status.awaitingPairing': 'Awaiting Pairing',
'status.pairingShort': 'Pairing',
// Dashboard
'dashboard.connections': 'Connections',
+2
View File
@@ -54,7 +54,9 @@ I18n.register('zh-CN', {
'status.restart': '重启',
'status.active': '已激活',
'status.installed': '已安装',
'status.configured': '已配置',
'status.awaitingPairing': '等待配对',
'status.pairingShort': '配对中',
// 仪表盘
'dashboard.connections': '连接数',
+27 -14
View File
@@ -2961,15 +2961,18 @@ body {
/* WASM channel setup stepper */
.ext-stepper {
display: flex;
align-items: center;
align-items: flex-start;
gap: 0;
margin: 8px 0 4px;
min-width: 0;
}
.stepper-step {
display: flex;
align-items: center;
gap: 4px;
gap: 6px;
min-width: 0;
flex: 1 1 0;
}
.stepper-circle {
@@ -2986,7 +2989,10 @@ body {
.stepper-label {
font-size: var(--text-xs);
white-space: nowrap;
white-space: normal;
overflow-wrap: anywhere;
line-height: 1.25;
min-width: 0;
}
.stepper-step.completed .stepper-circle {
@@ -3043,7 +3049,8 @@ body {
height: 2px;
background: var(--border);
margin: 0 4px;
flex-shrink: 0;
flex: 0 0 20px;
align-self: center;
}
.stepper-connector.completed {
@@ -3249,15 +3256,6 @@ body {
border: 1px solid var(--border);
}
.configure-qr-image {
width: min(260px, 100%);
align-self: center;
border-radius: 10px;
background: white;
padding: 8px;
box-sizing: border-box;
}
.configure-verification-title {
font-size: var(--text-sm);
font-weight: 600;
@@ -3282,14 +3280,29 @@ body {
}
.configure-verification-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: fit-content;
padding: 10px 14px;
border-radius: 10px;
border: 1px solid var(--accent);
background: var(--accent-subtle);
color: var(--accent, var(--text-link, #4ea3ff));
font-size: var(--text-sm);
font-weight: 600;
text-decoration: none;
transition: background var(--transition-fast), transform 150ms var(--ease-spring);
}
.configure-verification-link:hover {
text-decoration: underline;
background: var(--badge-sandbox-bg);
transform: translateY(-1px);
text-decoration: none;
}
.configure-verification-link:active {
transform: scale(0.98);
}
.configure-inline-error {
+6 -1
View File
@@ -466,7 +466,12 @@ pub fn classify_wasm_channel_activation(
} else if !ext.authenticated {
ExtensionActivationStatus::Installed
} else if ext.active {
if has_paired || has_owner_binding {
// WeChat QR login already proves channel ownership and does not have a
// separate owner-binding/pairing phase like Telegram DM verification.
if ext.name == crate::extensions::wechat_login::WECHAT_CHANNEL_NAME
|| has_paired
|| has_owner_binding
{
ExtensionActivationStatus::Active
} else {
ExtensionActivationStatus::Pairing
+1 -1
View File
@@ -7062,7 +7062,7 @@ mod tests {
InteractiveLoginStartResult {
session_id: "wechat-session-1".to_string(),
status: "pending".to_string(),
message: "Scan the QR code in WeChat to finish connecting.".to_string(),
message: "Open the WeChat QR page to continue.".to_string(),
qr_code_url: Some("https://qr.example/one".to_string()),
instructions: Some(
"Keep this window open while you scan and confirm on your phone."
+1 -1
View File
@@ -135,7 +135,7 @@ fn build_pending_login(
let result = InteractiveLoginStartResult {
session_id,
status: "pending".to_string(),
message: "Scan the QR code in WeChat to finish connecting.".to_string(),
message: "Open the WeChat QR page to continue.".to_string(),
qr_code_url: Some(qr.qrcode_img_content),
instructions: Some(
"Keep this window open while you scan and confirm on your phone.".to_string(),