import React from 'react'; import HashNavigation from '../lib/navigation.js'; import { uploadLargeFile, getAbuseReasons, submitAbuseReport, getFeedbackConfig, submitFeedback } from '../lib/im-api.js'; import { readCachedData, writeCachedData } from '../lib/use-cached-data.js'; const navigate = (view, params) => { let hash = HashNavigation.setUrlSidePanel(window.location.hash, view); Object.entries(params || {}).forEach(([key, value]) => { hash = value ? HashNavigation.addUrlParam(hash, key, value) : HashNavigation.removeUrlParam(hash, key); }); HashNavigation.navigateTo(hash); }; export const SECURITY_SUPPORT_VIEWS = [ 'fraud-risk-warning', 'fraud-risk-confirmation', 'abuse-report-reasons', 'abuse-report-details', 'user-feedback' ]; function Icon({children}) { return ; } function Header({title, back}) { return

{title}

; } function Toast({message, onDone}) { React.useEffect(() => { if (!message) return undefined; const timer = window.setTimeout(onDone, 2200); return () => window.clearTimeout(timer); }, [message, onDone]); return message ?
{message}
: null; } // Backend has no "content risk scan" or "post-send fraud alert" mechanism (confirmed against // im_be/server/im_http.go and session.go -- grepping for 诈骗/反诈/110/fraud in the whole backend // only turns up abuse-report reason labels, no scanning logic and no system-notification event type // for it). The two reference images (反诈弹窗(安卓).png / 反诈弹窗(二次提示安卓).png) show generic // anti-fraud education content that isn't tied to any specific message or backend-driven trigger -- // there's no field anywhere (bootstrap, profile, settings) indicating "has this been shown before". // So this is implemented as a standalone, always-reachable safety-info page (not something that // pops up automatically), reusing the two designs' content/copy verbatim for their real interactive // counterpart: the pre-send URL risk check below (RiskConfirmDialog), which DOES have a real backend // (POST /security/urls/check + POST /security/risk-challenges/{id}/confirm). function FraudAwareness({secondary}) { // 这里必须带上 risk-dialog:本页的 ul/li/aside/.risk-emphasis/button.primary 全部样式都挂在 // base.css 的 .risk-dialog 下(14 条规则),而 .risk-info-page 在 base.css 里一条规则都没有—— // 只写 risk-info-page 的话整页会渲染成带原生项目符号的裸列表。 // 图标名也修正了:shield_lock 只存在于 Material Symbols,index.html 加载的是经典 Material Icons, // 那套字体里没有这个字形,会原样显示 "shield_lock" 这串文本。改用仓库已在用的 verified_user。 return
{secondary ? 'warning_amber' : 'verified_user'} {secondary ? <>

您发送的内容可能涉及诈骗风险,请注意甄别。

切勿轻信陌生人的投资理财、兼职刷单、网络贷款等信息,谨防上当受骗。

: <>

为保障您的账号及资金安全,请您注意:

  • receipt_long不参与刷单返利活动
  • currency_yuan不轻信高收益投资、理财信息
  • person_off不出租、出借、出售账号及实名信息
  • lock不向任何人泄露验证码、密码等敏感信息

请提高安全防范意识,谨防电信网络诈骗。

}
; } // Exported for messages-view.jsx to render inline when composing a message that contains a URL -- // this is the one piece of Ch.11 with a genuine backend mechanism behind it (checkURL/confirmRiskChallenge, // verified against im_http.go:1184-1285). `level`/`blocked` come straight from the checkURL response. export function RiskConfirmDialog({level, blocked, onConfirm, onCancel}) { return
{blocked ? 'block' : 'warning_amber'}

安全提醒

{blocked ?

检测到该内容存在较高安全风险,暂无法发送。

: <>

您发送的内容可能涉及诈骗风险,请注意甄别。

切勿轻信陌生人的投资理财、兼职刷单、网络贷款等信息,谨防上当受骗。如遇诈骗请立即拨打 110 报警。

} {blocked ? : <>}
; } // target_type is one of user/group/message only (verified against server/store/store.go's // AbuseReasons validation and im_http.go's normalizeIMAbuseTarget switch -- there is no // "group_member" variant server-side; reporting a group member uses target_type=user and simply // loses the "which group" context, which is a real backend limitation, not something fixable here). function ReportReasons({hashParams, onToast}) { const targetType = (hashParams && hashParams.target_type) || 'user'; const targetName = (hashParams && hashParams.target_name) || ''; const [reasons, setReasons] = React.useState(null); React.useEffect(() => { getAbuseReasons(targetType).then(data => setReasons(Array.isArray(data) ? data : (data && (data.reasons || data.items)) || [])) .catch(err => { onToast(err.message || '加载失败'); setReasons([]); }); }, [targetType]); const select = reason => navigate('abuse-report-details', {...hashParams, reason_code: reason.reason_code || reason.code, reason_label: reason.label, requires_description: reason.requires_description ? '1' : ''}); {/* 'personal-center' made sense when contact-management-view.jsx's friend-settings page was the only entry point; tinode-web.jsx's handleReportTopic (block-and-report from inside a chat, which closes that chat before landing here) now also routes here, and for that origin 'personal-center' is a non-sequitur -- messages-home is a reasonable default for both. */} return
{targetName ?
account_circle举报{targetName}举报人身份不会向对方公开
: null}

请选择举报原因

{reasons === null ?

加载中…

:
{reasons.map(reason => )}
}
; } function ReportDetails({hashParams, tinode, onToast}) { const reasonLabel = (hashParams && hashParams.reason_label) || ''; const reasonCode = (hashParams && hashParams.reason_code) || ''; const requiresDescription = (hashParams && hashParams.requires_description) === '1'; const [description, setDescription] = React.useState(''); const [evidence, setEvidence] = React.useState([]); const [uploading, setUploading] = React.useState(false); const [submitting, setSubmitting] = React.useState(false); const [submitted, setSubmitted] = React.useState(null); const fileInputRef = React.useRef(null); const addEvidence = event => { const file = event.target.files && event.target.files[0]; event.target.value = ''; if (!file || !tinode) return; if (evidence.length >= 9) { onToast('最多上传 9 个证据文件'); return; } setUploading(true); uploadLargeFile(tinode, file).then(({fileId}) => setEvidence(items => items.concat({id: fileId, name: file.name}))) .catch(err => onToast(err.message || '上传失败')).finally(() => setUploading(false)); }; const submit = () => { if (requiresDescription && !description.trim()) { onToast('请补充问题描述'); return; } const target = {type: hashParams.target_type || 'user'}; if (hashParams.target_type === 'message') { target.topic = hashParams.topic; target.seq = String(hashParams.seq); } else if (hashParams.target_type === 'group') { target.topic = hashParams.topic || hashParams.target_id; } else { target.user_id = hashParams.target_id; } setSubmitting(true); submitAbuseReport({target, reason_code: reasonCode, description: description.trim(), evidence_file_ids: evidence.map(e => e.id)}) .then(result => setSubmitted(result)) .catch(err => onToast(err.message || '提交失败')) .finally(() => setSubmitting(false)); }; return
{submitted ?
task_alt

举报已受理

我们会依据平台规则核实处理,举报人身份不会向被举报方公开。

受理编号
{submitted.report_id}
:
举报原因{reasonLabel}