修复全页面走查发现的 P0 前端问题(第一批)
八个 agent 对 11 章逐页对照设计稿+后端源码后,先修其中"功能不可用/崩溃/谎报成功"级别的:
崩溃与数据错误
- content-library-view: Highlight 按 [start,len] 数组解构后端返回的 {start,end} 对象,
对象不可迭代会直接抛 TypeError,任何文本搜索命中都让整个结果区渲染崩掉。服务端偏移还叠加
两个问题(Go 字节下标 vs JS UTF-16;偏移基准是全文但 snippet 会被开窗截断),改为在展示
文本上本地匹配关键词。
- content-library-view: firstAttachment 用 Drafty.attachments,SDK 里硬过滤 tp=='EX'
只认文件附件,图片(IM)/视频(VD)/语音(AU) 一个都取不到——收藏缩略图、语音播放源、聊天记录
图片视频宫格全部拿不到资源。改用 Drafty.entities 按类型分派(chat-message.jsx 已有先例)。
- message-center-view: 系统通知的机器码兜底会把真实数据吞掉(日报提醒的 summary 就是日期
字符串 "2026-08-20",会议通知的 summary 是标题),改为查表命中才替换。
- message-center-view: 会话列表漏了 avatar 字段,每行都只能落到兜底图标;同文件
frequentContacts() 本来就是对的,照它补上。
功能不可用
- auth-onboarding-view: 首次开启青少年模式是死路——设完密码丢弃了服务端返回的
enable_action_token,从不调 enableYouthMode,用户走完短信验证+设密码后模式仍是关的。
现在拿着这个 token 把最后一步做完。
- group-management-view: "投诉/举报"是个 onToast('暂未开放') 死桩,而 target_type='group'
这条链路早就通了。接上真实举报页。
- tinode-web: 刷新/深链会被弹回首页。两个原因叠加:初始化白名单还是上游 Tinode 的
['cred','reset','register'];handleLoginSuccessful 无条件把 hash 改写成 workbench,
刷新触发的自动登录也走这条。抽出 SIDE_PANEL_VIEWS 常量复用,并区分登录流程与自动登录。
- widgets/meta-message: deleted 分支从未实现,撤回一条消息后时间线上什么都不剩。
谎报成功 / 判定错误
- tinode-web + personal-settings-view: 改密码不返回 Promise,服务端拒绝时照样弹"密码已更新"。
- group-management-view: 禁言批量读 result.succeeded,但该接口返回的是 {topic, items},
succeeded 恒为 undefined,不管成没成都弹"设置成功"。两个接口结构不同,分开判定。
- workplace-collaboration-view: 会议取消状态写成 'cancelled',后端是 'canceled' 单 l,
会议取消后"取消会议"按钮还挂着。
样式与显示
- base.css: 删掉旧版 .organization-department 的 padding-left !important,它压过按层级算出的
内联缩进,导致部门行全平、只有人员行缩进。
- base.css: 补 .youth-password-gate(原来 0 条规则),PIN 框缺 position:relative 和居中。
- security-support-view: shield_lock 不在经典 Material Icons 里(只有 Symbols),会显示成
字面文本;.risk-info-page 也是 0 条规则,整页渲染成裸项目符号列表。改用 verified_user 并
补上 .risk-dialog。
- contact-management-view: 好友资料页性别直接输出后端枚举,显示成 "male"。
- workplace-collaboration-view: 层级标注两条路径两种格式(树形深三层变"3级"、搜索硬传"三"),
统一成一个 helper。
已记录不在前端修的后端缺口:recallMessage 的 version 只能传 1,因为所有消息读取接口
(messagePayload/messageSearchPayload/mediaMessagePayload/favoriteSnapshot)都不下发 version,
冲突错误里也不回传真实值,被编辑过的消息撤回必然失败。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
c4189646f4
commit
5a07bb4e3b
+25
-15
@@ -653,21 +653,11 @@
|
|||||||
color: #8b929b;
|
color: #8b929b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.organization-root > .material-icons:first-child {
|
/* 旧版组织架构的 .organization-root / .organization-department / .organization-members 三条规则
|
||||||
color: #2f73d2;
|
已删除:前两个类名 JSX 里早就没人用了,而 .organization-department 那条
|
||||||
}
|
`padding-left: 1.75rem !important` 会压过新版按层级算出来的内联 paddingLeft,
|
||||||
|
导致一级/二级/三级部门行左边距完全一样、树形层级看不出来(人员行没有 !important,缩进正常,
|
||||||
.organization-department {
|
结果是"部门全平、人员缩进",比不缩进更乱)。新版完整规则在下面 .organization-department 那一块。 */
|
||||||
padding-left: 1.75rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.organization-department > .material-icons:first-child {
|
|
||||||
color: #e79a32;
|
|
||||||
}
|
|
||||||
|
|
||||||
.organization-members button {
|
|
||||||
padding-left: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.organization-members img {
|
.organization-members img {
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
@@ -6440,6 +6430,26 @@ div.toast.show {
|
|||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 青少年模式的 PIN 页复用了下面的 .auth-pin-boxes / .auth-pin-input / .auth-primary,
|
||||||
|
但它自己的容器类 .youth-password-gate 原来在整个 base.css 里一条规则都没有。缺的正是
|
||||||
|
.auth-pin-content 提供的两件事:position:relative(.auth-pin-input 是 position:absolute
|
||||||
|
+ top:11rem,没有定位祖先就会跑到更外层去,点 PIN 框重新聚焦会点空)和 align-items:center
|
||||||
|
(.auth-pin-boxes 是 grid,父级的 text-align:center 对它无效,不会居中)。 */
|
||||||
|
.youth-password-gate {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: min(100%, 32rem);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 3rem 2rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.youth-password-gate .auth-primary {
|
||||||
|
width: min(100%, 24rem);
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.auth-pin-boxes {
|
.auth-pin-boxes {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 4rem);
|
grid-template-columns: repeat(4, 4rem);
|
||||||
|
|||||||
@@ -449,6 +449,11 @@ export const getConversationSettings = topic => imGet('/conversations/' + encode
|
|||||||
export const updateConversationSettings = (topic, body) => imPatch('/conversations/' + encodeURIComponent(topic) + '/settings', body);
|
export const updateConversationSettings = (topic, body) => imPatch('/conversations/' + encodeURIComponent(topic) + '/settings', body);
|
||||||
|
|
||||||
// -- Messages (Ch.4/7). --
|
// -- Messages (Ch.4/7). --
|
||||||
|
// version 只能默认传 1:服务端做的是乐观锁校验(tenant_adapter.go:1963 要求 msg.Version 与传入值
|
||||||
|
// 完全相等,否则 ErrDuplicate),但**没有任何消息读取接口下发过 version 字段**——messagePayload /
|
||||||
|
// messageSearchPayload / mediaMessagePayload / favoriteSnapshot 全都没有,冲突时的错误里也不回传
|
||||||
|
// 真实值。所以前端拿不到正确的版本号,被编辑过(version>1)的消息撤回必然失败。这是后端缺口,
|
||||||
|
// 已记入《IM-后端问题清单.md》,不在前端绕。
|
||||||
export const recallMessage = (topic, seq, version = 1, reason = 'mistake') =>
|
export const recallMessage = (topic, seq, version = 1, reason = 'mistake') =>
|
||||||
imPost('/topics/' + encodeURIComponent(topic) + '/messages/' + encodeURIComponent(seq) + '/recall', {version, reason});
|
imPost('/topics/' + encodeURIComponent(topic) + '/messages/' + encodeURIComponent(seq) + '/recall', {version, reason});
|
||||||
export const reviseMessage = (topic, seq, content, version = 1) =>
|
export const reviseMessage = (topic, seq, content, version = 1) =>
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import HashNavigation from '../lib/navigation.js';
|
|||||||
import {
|
import {
|
||||||
resolveTenant, submitTenantApplication, submitTenantEntryFeedback, uploadPublicFile,
|
resolveTenant, submitTenantApplication, submitTenantEntryFeedback, uploadPublicFile,
|
||||||
requestVerificationCode, verifyRegisterCode, verifyPasswordResetCode, confirmPasswordReset,
|
requestVerificationCode, verifyRegisterCode, verifyPasswordResetCode, confirmPasswordReset,
|
||||||
createSecurityChallenge, verifySecurityChallenge, getRealnameStatus, submitRealname, setIndependentPassword
|
createSecurityChallenge, verifySecurityChallenge, getRealnameStatus, submitRealname, setIndependentPassword,
|
||||||
|
getYouthMode, enableYouthMode
|
||||||
} from '../lib/im-api.js';
|
} from '../lib/im-api.js';
|
||||||
|
|
||||||
const USERNAME_RE = /^[a-z0-9]{1,10}$/;
|
const USERNAME_RE = /^[a-z0-9]{1,10}$/;
|
||||||
@@ -674,9 +675,30 @@ function IndependentPassword({authenticated}) {
|
|||||||
if (!actionToken) { setError('身份验证已过期,请返回重新验证'); return; }
|
if (!actionToken) { setError('身份验证已过期,请返回重新验证'); return; }
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
await setIndependentPassword(pin, 'youth_mode', actionToken);
|
const result = await setIndependentPassword(pin, 'youth_mode', actionToken);
|
||||||
sessionStorage.removeItem('im-action-token:' + IDENTITY_CHECK_SCENE);
|
sessionStorage.removeItem('im-action-token:' + IDENTITY_CHECK_SCENE);
|
||||||
setError(''); setToast('独立密码设置成功'); setPin('');
|
setPin('');
|
||||||
|
// 设密码只是"首次开启青少年模式"这条路的中间一步,不是终点。服务端在这一步就把下一步
|
||||||
|
// 要用的凭证一起发回来了(enable_action_token + next_action:"enable_youth_mode",
|
||||||
|
// 见 server/im_http.go:6178-6189)。原来这里把它整个丢掉、只弹一句"设置成功"就停在
|
||||||
|
// PIN 页——用户走完短信验证+设密码,青少年模式其实还是关的,得自己摸回设置页再输一遍。
|
||||||
|
// 现在拿着这个 token 把最后一步做完。
|
||||||
|
const enableToken = result && result.enable_action_token;
|
||||||
|
if (enableToken) {
|
||||||
|
try {
|
||||||
|
const current = await getYouthMode();
|
||||||
|
await enableYouthMode(current.policy_version, enableToken);
|
||||||
|
setError(''); setToast('青少年模式已开启');
|
||||||
|
window.setTimeout(() => navigate('youth-mode-on'), 600);
|
||||||
|
return;
|
||||||
|
} catch (enableErr) {
|
||||||
|
// 密码确实设成功了,只是自动开启这一步没成,如实说明并把人送回青少年模式页手动开
|
||||||
|
setError('独立密码已设置,但自动开启未成功:' + (enableErr.message || '请返回重试'));
|
||||||
|
window.setTimeout(() => navigate('youth-mode-off'), 1200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setError(''); setToast('独立密码设置成功');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || '设置失败,请返回重新验证身份');
|
setError(err.message || '设置失败,请返回重新验证身份');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -191,6 +191,9 @@ function FriendRequests({onToast}) {
|
|||||||
</main></div>;
|
</main></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 与 personal-settings-view.jsx 的 GENDER_OPTIONS 一一对应,两处写的是同一套后端枚举
|
||||||
|
const GENDER_LABELS = {male: '男', female: '女', other: '其他'};
|
||||||
|
|
||||||
// `GET /users/{id}/profile` (getUserProfile) 403s for anyone who isn't already a friend, by design
|
// `GET /users/{id}/profile` (getUserProfile) 403s for anyone who isn't already a friend, by design
|
||||||
// (server's default `profile_visibility` is "friends"-only, server/im_http.go's imPrivacyAllowsProfile).
|
// (server's default `profile_visibility` is "friends"-only, server/im_http.go's imPrivacyAllowsProfile).
|
||||||
// But the two places that hand you a *stranger* to look at -- an account search hit and a scanned
|
// But the two places that hand you a *stranger* to look at -- an account search hit and a scanned
|
||||||
@@ -357,7 +360,9 @@ function Profile({uid, friend, onTopicSelected, onToast}) {
|
|||||||
<section><Avatar person={profile} /><div><h2>{profile.display_name}</h2>
|
<section><Avatar person={profile} /><div><h2>{profile.display_name}</h2>
|
||||||
{profile.public_account ? <p>用户名:{profile.public_account}</p> : null}
|
{profile.public_account ? <p>用户名:{profile.public_account}</p> : null}
|
||||||
{profile.region ? <p>地区:{profile.region}</p> : null}</div><Icon>qr_code</Icon></section>
|
{profile.region ? <p>地区:{profile.region}</p> : null}</div><Icon>qr_code</Icon></section>
|
||||||
<dl>{profile.gender ? <div><dt>性别</dt><dd>{profile.gender}</dd></div> : null}
|
{/* 后端存的是 male/female/other 枚举(personal-settings-view.jsx:203 的 GENDER_OPTIONS 写进去的),
|
||||||
|
原来直接原样输出,资料页上显示的是英文 "male"。 */}
|
||||||
|
<dl>{profile.gender ? <div><dt>性别</dt><dd>{GENDER_LABELS[profile.gender] || profile.gender}</dd></div> : null}
|
||||||
<div><dt>个性签名</dt><dd className={profile.signature ? '' : 'muted'}>{profile.signature || '此人很懒什么都没留下~'}</dd></div>
|
<div><dt>个性签名</dt><dd className={profile.signature ? '' : 'muted'}>{profile.signature || '此人很懒什么都没留下~'}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
{isFriend ? <div className="people-profile-actions">
|
{isFriend ? <div className="people-profile-actions">
|
||||||
|
|||||||
@@ -62,11 +62,23 @@ function Search({value, onChange, placeholder = '搜索'}) { return <label class
|
|||||||
function draftyContentOf(content) {
|
function draftyContentOf(content) {
|
||||||
return Drafty.isValid(content) ? content : null;
|
return Drafty.isValid(content) ? content : null;
|
||||||
}
|
}
|
||||||
|
// 必须用 Drafty.entities 而不是 Drafty.attachments:后者在 SDK 里硬过滤 `ent.tp == 'EX'`
|
||||||
|
// (tinode.dev.js:2538),只认「文件附件」这一种实体。而本应用发图片/视频/语音走的分别是
|
||||||
|
// insertImage(IM)/insertVideo(VD)/appendAudio(AU),在 attachments 下一个都取不到——收藏的图片视频
|
||||||
|
// 缩略图、语音收藏的播放源、聊天记录的图片视频宫格因此全部拿不到资源。entities 的回调第三参给出
|
||||||
|
// tp,可以按类型分派,chat-message.jsx:139 用的就是这个。
|
||||||
|
const MEDIA_ENTITY_TYPES = ['IM', 'VD', 'AU', 'EX'];
|
||||||
function firstAttachment(content) {
|
function firstAttachment(content) {
|
||||||
const drafty = draftyContentOf(content);
|
const drafty = draftyContentOf(content);
|
||||||
if (!drafty) return null;
|
if (!drafty) return null;
|
||||||
let found = null;
|
let found = null;
|
||||||
Drafty.attachments(drafty, att => { if (!found) found = att; });
|
Drafty.entities(drafty, (data, _index, tp) => {
|
||||||
|
if (!found && data && MEDIA_ENTITY_TYPES.includes(tp)) {
|
||||||
|
found = data;
|
||||||
|
return true; // 命中第一个就停,entities 的回调返回 true 表示中断遍历
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
// Drafty.getDownloadUrl returns either an already-usable blob: URL (small inband attachment, .val) or
|
// Drafty.getDownloadUrl returns either an already-usable blob: URL (small inband attachment, .val) or
|
||||||
@@ -388,14 +400,29 @@ function DownloadPage({hashParams, tinode, onToast}) {
|
|||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Highlight({text, ranges}) {
|
// 不用服务端返回的 matches 偏移,改为在展示文本上本地匹配关键词。服务端那份偏移有三层问题,
|
||||||
if (!ranges || !ranges.length) return text;
|
// 全都对不上前端要高亮的这段文字:
|
||||||
const parts = []; let last = 0;
|
// 1. 结构是 [{start,end}] 对象数组(im_http.go:9151),按 [start,len] 数组解构会直接抛
|
||||||
ranges.forEach(([start, len], i) => {
|
// TypeError(对象不可迭代),整个搜索结果渲染崩掉;
|
||||||
parts.push(text.slice(last, start), <mark key={i}>{text.slice(start, start + len)}</mark>);
|
// 2. start/end 是 Go 的字节下标(strings.Index + len(query)),中文一字三字节,跟 JS 的
|
||||||
last = start + len;
|
// UTF-16 下标永远错位;
|
||||||
});
|
// 3. 偏移基准是完整原文,但 snippet 在超过 120 字时会被开窗截断(im_http.go:9128-9139),
|
||||||
parts.push(text.slice(last));
|
// 两者基准不一致——而且那段开窗自己也混用了字节下标和 rune 切片。
|
||||||
|
// 本地匹配没有这些歧义:所见即所高亮。
|
||||||
|
function Highlight({text, query}) {
|
||||||
|
const needle = (query || '').trim();
|
||||||
|
if (!needle || !text) return text;
|
||||||
|
const parts = [];
|
||||||
|
const haystack = text.toLowerCase();
|
||||||
|
const lowered = needle.toLowerCase();
|
||||||
|
let last = 0;
|
||||||
|
for (let at = haystack.indexOf(lowered); at !== -1; at = haystack.indexOf(lowered, at + lowered.length)) {
|
||||||
|
if (at > last) parts.push(text.slice(last, at));
|
||||||
|
parts.push(<mark key={at}>{text.slice(at, at + needle.length)}</mark>);
|
||||||
|
last = at + needle.length;
|
||||||
|
}
|
||||||
|
if (!parts.length) return text;
|
||||||
|
if (last < text.length) parts.push(text.slice(last));
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,7 +521,7 @@ function History({view, hashParams, myUserId, onToast}) {
|
|||||||
view === 'history-search' ? <section className="library-message-results">
|
view === 'history-search' ? <section className="library-message-results">
|
||||||
{list.map(item => <button type="button" key={item.seq} onClick={() => jump(item.seq)}>
|
{list.map(item => <button type="button" key={item.seq} onClick={() => jump(item.seq)}>
|
||||||
<Avatar item={{}} /><span><strong>{nameOf(item.from)}</strong><p>{item.type === 'text' ?
|
<Avatar item={{}} /><span><strong>{nameOf(item.from)}</strong><p>{item.type === 'text' ?
|
||||||
<Highlight text={item.snippet || ''} ranges={item.matches} /> : (TYPE_LABEL[item.type] || item.snippet || '')}</p>
|
<Highlight text={item.snippet || ''} query={query} /> : (TYPE_LABEL[item.type] || item.snippet || '')}</p>
|
||||||
<small>{shortDateFormat(new Date(item.ts))}</small></span><Icon>chevron_right</Icon>
|
<small>{shortDateFormat(new Date(item.ts))}</small></span><Icon>chevron_right</Icon>
|
||||||
</button>)}
|
</button>)}
|
||||||
</section> :
|
</section> :
|
||||||
|
|||||||
@@ -396,7 +396,17 @@ function GroupSettings({topic, myUserId, tinode, onToast}) {
|
|||||||
const t = tinode ? tinode.getTopic(topic) : null;
|
const t = tinode ? tinode.getTopic(topic) : null;
|
||||||
(t ? t.delMessagesAll(false) : Promise.resolve()).then(() => onToast('聊天记录已清除')).catch(err => onToast(err.message || '操作失败'));
|
(t ? t.delMessagesAll(false) : Promise.resolve()).then(() => onToast('聊天记录已清除')).catch(err => onToast(err.message || '操作失败'));
|
||||||
}}><span>清空聊天记录</span><Icon>chevron_right</Icon></button>
|
}}><span>清空聊天记录</span><Icon>chevron_right</Icon></button>
|
||||||
<button type="button" onClick={() => onToast('举报入口暂未开放')}><span>投诉/举报</span><Icon>chevron_right</Icon></button>
|
{/* target_type='group' 这条链路早就是通的(tinode-web.jsx 的 handleReportTopic 就在用),
|
||||||
|
这里原来一直是个"暂未开放"的死桩。跟 contact-management-view.jsx 的好友举报入口一样,
|
||||||
|
举报页要的是 target_type/target_id/target_name 三个参数,本文件的 navigate 辅助函数
|
||||||
|
只会写 topic,所以直接拼 hash。 */}
|
||||||
|
<button type="button" onClick={() => {
|
||||||
|
let hash = HashNavigation.setUrlSidePanel('', 'abuse-report-reasons');
|
||||||
|
hash = HashNavigation.addUrlParam(hash, 'target_type', 'group');
|
||||||
|
hash = HashNavigation.addUrlParam(hash, 'target_id', topic);
|
||||||
|
hash = HashNavigation.addUrlParam(hash, 'target_name', (group && group.name) || topic);
|
||||||
|
HashNavigation.navigateTo(hash);
|
||||||
|
}}><span>投诉/举报</span><Icon>chevron_right</Icon></button>
|
||||||
</section>
|
</section>
|
||||||
<button type="button" className="group-danger" onClick={leave}>退出群组</button>
|
<button type="button" className="group-danger" onClick={leave}>退出群组</button>
|
||||||
</main>
|
</main>
|
||||||
@@ -659,8 +669,19 @@ function MemberPicker({type, topic, onToast}) {
|
|||||||
setBusy(true);
|
setBusy(true);
|
||||||
const call = mute ? muteGroupMembers(topic, selected.map(user_id => ({user_id}))) : addGroupAdmins(topic, selected);
|
const call = mute ? muteGroupMembers(topic, selected.map(user_id => ({user_id}))) : addGroupAdmins(topic, selected);
|
||||||
call.then(result => {
|
call.then(result => {
|
||||||
const failed = result && result.succeeded ? result.succeeded.filter(s => !s.ok) : [];
|
// 两个批量接口的响应结构不一样,不能用同一套判定:
|
||||||
setToast(failed.length ? `成功设置 ${selected.length - failed.length} 人,${failed.length} 人设置失败` : '设置成功');
|
// - 添加管理员 POST .../admins/batch 返回 succeeded[],每项带 ok/reason(im_http.go:5377-5392)
|
||||||
|
// - 添加禁言 POST .../mutes 返回 {topic, items},只有成功的才进 items,
|
||||||
|
// 非法 uid 会被静默 continue(im_http.go:5486-5488),根本没有 succeeded 字段
|
||||||
|
// 原来两条路都读 succeeded,禁言那条恒为 undefined → failed 恒为空 → 不管实际成没成都弹"设置成功"。
|
||||||
|
let failedCount;
|
||||||
|
if (mute) {
|
||||||
|
const done = (result && Array.isArray(result.items)) ? result.items.length : selected.length;
|
||||||
|
failedCount = Math.max(selected.length - done, 0);
|
||||||
|
} else {
|
||||||
|
failedCount = (result && Array.isArray(result.succeeded)) ? result.succeeded.filter(s => !s.ok).length : 0;
|
||||||
|
}
|
||||||
|
setToast(failedCount ? `成功设置 ${selected.length - failedCount} 人,${failedCount} 人设置失败` : '设置成功');
|
||||||
window.setTimeout(() => navigate(back, topic), 900);
|
window.setTimeout(() => navigate(back, topic), 900);
|
||||||
}).catch(err => onToast(err.message || '设置失败')).finally(() => setBusy(false));
|
}).catch(err => onToast(err.message || '设置失败')).finally(() => setBusy(false));
|
||||||
};
|
};
|
||||||
@@ -867,8 +888,10 @@ function GroupDescriptionEditor({topic, onToast}) {
|
|||||||
return <div className="group-page">
|
return <div className="group-page">
|
||||||
<Header title="群简介" back="group-profile" backTopic={topic} right="发布" onRight={publish} />
|
<Header title="群简介" back="group-profile" backTopic={topic} right="发布" onRight={publish} />
|
||||||
<main className="group-edit-text">
|
<main className="group-edit-text">
|
||||||
<textarea autoFocus value={value} maxLength={225} placeholder="请输入群简介内容" onChange={e => setValue(e.target.value)} />
|
{/* 上限跟后端硬限对齐(im_http.go:4667 超过 200 rune 直接 400 field=description)。
|
||||||
<small>{value.length}/225</small>
|
参考图标的是 0/225,但那个数字后端不接受——按 225 放行会让用户写满再被服务端打回。 */}
|
||||||
|
<textarea autoFocus value={value} maxLength={200} placeholder="请输入群简介内容" onChange={e => setValue(e.target.value)} />
|
||||||
|
<small>{value.length}/200</small>
|
||||||
</main>
|
</main>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,6 +147,9 @@ function normalizeConversations(chatList, myUserId) {
|
|||||||
summary,
|
summary,
|
||||||
time: formatConversationTime(item.touched),
|
time: formatConversationTime(item.touched),
|
||||||
unread: Math.max(Number(item.unread) || 0, 0),
|
unread: Math.max(Number(item.unread) || 0, 0),
|
||||||
|
// 真实头像:跟同文件 frequentContacts() 一样从 public.photo 取,原来漏了这一项,
|
||||||
|
// 导致会话列表每一行都只能落到下面的兜底图标,永远显示不出头像。
|
||||||
|
avatar: makeImageUrl(item.public && item.public.photo),
|
||||||
// 没有真实头像时的兜底图标要区分群聊和单聊,否则群聊也画成单人像,列表里分不出哪条是群
|
// 没有真实头像时的兜底图标要区分群聊和单聊,否则群聊也画成单人像,列表里分不出哪条是群
|
||||||
icon: isGroup ? 'group' : 'person',
|
icon: isGroup ? 'group' : 'person',
|
||||||
// 在线状态只对单聊有意义:群不存在"在线",传 undefined 让 Avatar 不渲染那个状态点
|
// 在线状态只对单聊有意义:群不存在"在线",传 undefined 让 Avatar 不渲染那个状态点
|
||||||
@@ -310,15 +313,15 @@ const NOTIFICATION_TEXT = {
|
|||||||
rejected: '对方拒绝了你的好友申请'
|
rejected: '对方拒绝了你的好友申请'
|
||||||
};
|
};
|
||||||
|
|
||||||
// 机器码判定:纯 ASCII 且只由小写字母/数字/点/下划线/连字符组成("accepted"、
|
|
||||||
// "group.settings.updated" 这类),正常中文文案不会命中。
|
|
||||||
const MACHINE_CODE = /^[a-z0-9._-]+$/;
|
|
||||||
|
|
||||||
function systemNotificationText(item) {
|
function systemNotificationText(item) {
|
||||||
const summary = (item.summary || '').trim();
|
const summary = (item.summary || '').trim();
|
||||||
// 后端给了真正的文案就直接用;只有当它是机器码时才走兜底,避免把未来后端补上的正常文案盖掉。
|
// 只在查表命中时才替换。后端的 summary 有些确实是机器码(好友申请通过时是 "accepted"),
|
||||||
if (summary && !MACHINE_CODE.test(summary)) return summary;
|
// 但也有些就是真实数据——日报提醒的 summary 是纯日期字符串
|
||||||
return NOTIFICATION_TEXT[summary] || NOTIFICATION_TEXT[item.event_type] || '您有一条新的系统通知。';
|
// (db/mysql/tenant_adapter.go:4917 传 date.Format("2006-01-02")),会议通知的 summary 是会议标题。
|
||||||
|
// 一旦按"看起来像机器码就吞掉"来判断,这些真实内容会被换成一句无意义的兜底文案。
|
||||||
|
const mapped = NOTIFICATION_TEXT[summary] || NOTIFICATION_TEXT[item.event_type];
|
||||||
|
if (mapped) return mapped;
|
||||||
|
return summary || '您有一条新的系统通知。';
|
||||||
}
|
}
|
||||||
|
|
||||||
function SystemMessages({authenticated}) {
|
function SystemMessages({authenticated}) {
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ const SHOW_GO_TO_LAST_DIST = 100;
|
|||||||
const FETCH_PAGE_TRIGGER = 40;
|
const FETCH_PAGE_TRIGGER = 40;
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
// 已存在于 src/i18n/*.json(zh: "消息已删除"),这里只是注册引用,不是新增待翻译串
|
||||||
|
message_deleted: {
|
||||||
|
id: 'message_deleted',
|
||||||
|
defaultMessage: 'message deleted',
|
||||||
|
description: 'Placeholder shown in the timeline where a message was recalled or deleted',
|
||||||
|
},
|
||||||
online_now: {
|
online_now: {
|
||||||
id: 'online_now',
|
id: 'online_now',
|
||||||
defaultMessage: 'online now',
|
defaultMessage: 'online now',
|
||||||
@@ -1997,6 +2003,7 @@ class MessagesView extends React.Component {
|
|||||||
messageNodes.push(
|
messageNodes.push(
|
||||||
<MetaMessage
|
<MetaMessage
|
||||||
deleted={true}
|
deleted={true}
|
||||||
|
deletedText={this.props.intl.formatMessage(messages.message_deleted)}
|
||||||
key={msg.seq} />
|
key={msg.seq} />
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -383,13 +383,22 @@ function ChangePassword({onUpdatePassword, onToast}) {
|
|||||||
const [oldPwd, setOldPwd] = React.useState('');
|
const [oldPwd, setOldPwd] = React.useState('');
|
||||||
const [newPwd, setNewPwd] = React.useState('');
|
const [newPwd, setNewPwd] = React.useState('');
|
||||||
const [confirmPwd, setConfirmPwd] = React.useState('');
|
const [confirmPwd, setConfirmPwd] = React.useState('');
|
||||||
|
const [busy, setBusy] = React.useState(false);
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
if (!oldPwd || newPwd.length < 6) { onToast('请输入原密码,新密码至少6位'); return; }
|
if (!oldPwd || newPwd.length < 6) { onToast('请输入原密码,新密码至少6位'); return; }
|
||||||
if (newPwd !== confirmPwd) { onToast('两次输入的新密码不一致'); return; }
|
if (newPwd !== confirmPwd) { onToast('两次输入的新密码不一致'); return; }
|
||||||
if (!onUpdatePassword) { onToast('当前环境不支持修改密码'); return; }
|
if (!onUpdatePassword) { onToast('当前环境不支持修改密码'); return; }
|
||||||
onUpdatePassword(newPwd);
|
if (busy) return;
|
||||||
onToast('密码已更新');
|
setBusy(true);
|
||||||
window.setTimeout(() => navigate('android-settings'), 500);
|
// 等真实结果再报成功。原来是发出去就立刻弹"密码已更新"并跳走,服务端拒绝(密码策略不符等)
|
||||||
|
// 时用户看到的仍然是成功提示,密码其实没变。
|
||||||
|
Promise.resolve(onUpdatePassword(newPwd)).then(() => {
|
||||||
|
onToast('密码已更新');
|
||||||
|
window.setTimeout(() => navigate('android-settings'), 500);
|
||||||
|
}).catch(err => {
|
||||||
|
onToast((err && err.message) || '修改失败,请重试');
|
||||||
|
setBusy(false);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
return <div className="personal-page profile-editor-page"><Header title="修改密码" back="android-settings" action="完成" onAction={submit} /><main>
|
return <div className="personal-page profile-editor-page"><Header title="修改密码" back="android-settings" action="完成" onAction={submit} /><main>
|
||||||
<label><span>原密码</span><input type="password" value={oldPwd} onChange={e => setOldPwd(e.target.value)} placeholder="请输入当前密码" /></label>
|
<label><span>原密码</span><input type="password" value={oldPwd} onChange={e => setOldPwd(e.target.value)} placeholder="请输入当前密码" /></label>
|
||||||
|
|||||||
@@ -43,8 +43,13 @@ function Toast({message, onDone}) {
|
|||||||
// counterpart: the pre-send URL risk check below (RiskConfirmDialog), which DOES have a real backend
|
// 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).
|
// (POST /security/urls/check + POST /security/risk-challenges/{id}/confirm).
|
||||||
function FraudAwareness({secondary}) {
|
function FraudAwareness({secondary}) {
|
||||||
return <div className="support-page risk-warning-page"><Header title="安全提醒" back={secondary ? 'fraud-risk-warning' : 'personal-center'} /><main className="risk-info-page">
|
// 这里必须带上 risk-dialog:本页的 ul/li/aside/.risk-emphasis/button.primary 全部样式都挂在
|
||||||
<span className="risk-symbol"><Icon>{secondary ? 'warning_amber' : 'shield_lock'}</Icon></span>
|
// 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 <div className="support-page risk-warning-page"><Header title="安全提醒" back={secondary ? 'fraud-risk-warning' : 'personal-center'} /><main className="risk-info-page risk-dialog">
|
||||||
|
<span className="risk-symbol"><Icon>{secondary ? 'warning_amber' : 'verified_user'}</Icon></span>
|
||||||
{secondary ? <>
|
{secondary ? <>
|
||||||
<p className="risk-emphasis">您发送的内容可能涉及诈骗风险,请注意甄别。</p>
|
<p className="risk-emphasis">您发送的内容可能涉及诈骗风险,请注意甄别。</p>
|
||||||
<p>切勿轻信陌生人的投资理财、兼职刷单、网络贷款等信息,谨防上当受骗。</p>
|
<p>切勿轻信陌生人的投资理财、兼职刷单、网络贷款等信息,谨防上当受骗。</p>
|
||||||
|
|||||||
+74
-48
@@ -101,6 +101,59 @@ const messages = defineMessages({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 全部合法的侧栏视图名。抽成模块级常量而不是内联在 handleHashRoute 里,是因为初始化时的
|
||||||
|
// "要不要保留 hash" 判断也需要同一份名单——那里原来写的是上游 Tinode 遗留的
|
||||||
|
// ['cred','reset','register'] 三个名字,本工程新增的上百个路由一个都不在里面,导致带任何新路由的
|
||||||
|
// 地址刷新/深链进入都会被 navigateTo('') 清空 hash 弹回首页。
|
||||||
|
const SIDE_PANEL_VIEWS = ['acc_general','archive','blocked','contacts','cred','crop','edit','general',
|
||||||
|
'newtpk','notif','register','reset','security','settings','support','wallpapers',
|
||||||
|
'tenant','login','enterprise-register','enterprise-feedback','identity-check',
|
||||||
|
'realname','realname-verified','realname-reminder','independent-password',
|
||||||
|
'messages-home','messages-offline','messages-menu','message-actions','system-messages',
|
||||||
|
'chat-inbound-request','chat-inbound-message','chat-outbound-request','chat-status',
|
||||||
|
'chat-friend','chat-media','chat-blocked','chat-deleted','chat-standard',
|
||||||
|
'chat-attachments','chat-mixed','chat-contact-card',
|
||||||
|
'compose-keyboard','mentions-select','mentions-complete','mentions-message',
|
||||||
|
'emoji-panel','emoji-message','voice-ready','voice-hold','voice-recording',
|
||||||
|
'media-uploading','media-gallery','media-retry','upload-file','file-message',
|
||||||
|
'message-multiselect','favorite-success','forward-select','forward-multiple',
|
||||||
|
'forward-individual','forward-bundle',
|
||||||
|
'address-book','friend-requests','add-friend','my-friend-qr','friend-search-result',
|
||||||
|
'friend-verification','friend-profile','group-member-profile','friend-settings',
|
||||||
|
'friend-settings-blocked','friend-delete-confirm','block-list','block-confirm','clear-chat-confirm',
|
||||||
|
'group-list','group-empty','group-invite','group-events','group-join-requests',
|
||||||
|
'group-new-request','group-announcement','group-summary','group-announcement-detail',
|
||||||
|
'group-settings','group-profile','group-avatar','group-nickname','group-description',
|
||||||
|
'group-my-nickname','group-role-message','group-qr','group-governance','group-admins','group-admin-add',
|
||||||
|
'group-admin-success','group-admin-partial','group-admin-removed','group-mute-add',
|
||||||
|
'group-muted','group-owner-select','group-owner-confirm','group-files','group-file-upload',
|
||||||
|
'favorites','favorites-mixed','favorites-files','favorites-text','favorites-voice',
|
||||||
|
'favorites-links','favorites-select','favorite-share','file-download','file-downloading',
|
||||||
|
'file-downloaded','history-search','history-files','history-links','history-images','history-videos',
|
||||||
|
'call-history','call-incoming','audio-call-ringing','audio-call-active','audio-call-muted',
|
||||||
|
'audio-call-speaker','video-call-ringing','video-call-camera-off','video-call-active',
|
||||||
|
'video-call-peer-camera-off','call-minimized',
|
||||||
|
'workbench','organization-directory','meeting-organizer','meeting-attendee',
|
||||||
|
'meeting-create','meeting-detail',
|
||||||
|
'daily-report-editor','daily-report-mine','daily-report-team','daily-report-detail',
|
||||||
|
'monthly-report-editor','monthly-report-list','monthly-report-detail',
|
||||||
|
'executive-mailbox','executive-mailbox-notice',
|
||||||
|
'personal-center','personal-profile','profile-nickname','profile-birth-date',
|
||||||
|
'profile-signature','profile-gender','profile-region','personal-qrcode','android-settings',
|
||||||
|
'cache-clear-confirm','clear-history-confirm','change-password','delete-account-confirm',
|
||||||
|
'app-version','privacy-settings','friend-discovery-settings',
|
||||||
|
'youth-mode-off','youth-mode-on','youth-mode-verify',
|
||||||
|
'notification-permission','permission-settings-reminder','camera-permission-info',
|
||||||
|
'microphone-permission-info',
|
||||||
|
'fraud-risk-warning','fraud-risk-confirmation','abuse-report-reasons',
|
||||||
|
'abuse-report-details','user-feedback',
|
||||||
|
''];
|
||||||
|
|
||||||
|
// 登录流程本身的页面。从这些页面登录成功后应该落到工作台;从任何其它页面(刷新触发的自动登录)
|
||||||
|
// 则应该留在原处。
|
||||||
|
const AUTH_FLOW_VIEWS = ['tenant', 'login', 'register', 'reset', 'cred',
|
||||||
|
'enterprise-register', 'enterprise-feedback'];
|
||||||
|
|
||||||
class TinodeWeb extends React.Component {
|
class TinodeWeb extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -431,7 +484,7 @@ class TinodeWeb extends React.Component {
|
|||||||
// Parse the hash navigation params.
|
// Parse the hash navigation params.
|
||||||
const parsedNav = HashNavigation.parseUrlHash(window.location.hash);
|
const parsedNav = HashNavigation.parseUrlHash(window.location.hash);
|
||||||
// Maybe navigate to home screen.
|
// Maybe navigate to home screen.
|
||||||
if (!['cred', 'reset', 'register'].includes(parsedNav.path[0])) {
|
if (!SIDE_PANEL_VIEWS.includes(parsedNav.path[0]) || !parsedNav.path[0]) {
|
||||||
// Save possible topic name.
|
// Save possible topic name.
|
||||||
this.setState({requestedTopic: parsedNav.path[1]});
|
this.setState({requestedTopic: parsedNav.path[1]});
|
||||||
const path = parsedNav.params && parsedNav.params.cred_done ?
|
const path = parsedNav.params && parsedNav.params.cred_done ?
|
||||||
@@ -650,49 +703,7 @@ class TinodeWeb extends React.Component {
|
|||||||
|
|
||||||
if (hash.path && hash.path.length > 0) {
|
if (hash.path && hash.path.length > 0) {
|
||||||
// Left-side panel selector.
|
// Left-side panel selector.
|
||||||
if (['acc_general','archive','blocked','contacts','cred','crop','edit','general',
|
if (SIDE_PANEL_VIEWS.includes(hash.path[0])) {
|
||||||
'newtpk','notif','register','reset','security','settings','support','wallpapers',
|
|
||||||
'tenant','login','enterprise-register','enterprise-feedback','identity-check',
|
|
||||||
'realname','realname-verified','realname-reminder','independent-password',
|
|
||||||
'messages-home','messages-offline','messages-menu','message-actions','system-messages',
|
|
||||||
'chat-inbound-request','chat-inbound-message','chat-outbound-request','chat-status',
|
|
||||||
'chat-friend','chat-media','chat-blocked','chat-deleted','chat-standard',
|
|
||||||
'chat-attachments','chat-mixed','chat-contact-card',
|
|
||||||
'compose-keyboard','mentions-select','mentions-complete','mentions-message',
|
|
||||||
'emoji-panel','emoji-message','voice-ready','voice-hold','voice-recording',
|
|
||||||
'media-uploading','media-gallery','media-retry','upload-file','file-message',
|
|
||||||
'message-multiselect','favorite-success','forward-select','forward-multiple',
|
|
||||||
'forward-individual','forward-bundle',
|
|
||||||
'address-book','friend-requests','add-friend','my-friend-qr','friend-search-result',
|
|
||||||
'friend-verification','friend-profile','group-member-profile','friend-settings',
|
|
||||||
'friend-settings-blocked','friend-delete-confirm','block-list','block-confirm','clear-chat-confirm',
|
|
||||||
'group-list','group-empty','group-invite','group-events','group-join-requests',
|
|
||||||
'group-new-request','group-announcement','group-summary','group-announcement-detail',
|
|
||||||
'group-settings','group-profile','group-avatar','group-nickname','group-description',
|
|
||||||
'group-my-nickname','group-role-message','group-qr','group-governance','group-admins','group-admin-add',
|
|
||||||
'group-admin-success','group-admin-partial','group-admin-removed','group-mute-add',
|
|
||||||
'group-muted','group-owner-select','group-owner-confirm','group-files','group-file-upload',
|
|
||||||
'favorites','favorites-mixed','favorites-files','favorites-text','favorites-voice',
|
|
||||||
'favorites-links','favorites-select','favorite-share','file-download','file-downloading',
|
|
||||||
'file-downloaded','history-search','history-files','history-links','history-images','history-videos',
|
|
||||||
'call-history','call-incoming','audio-call-ringing','audio-call-active','audio-call-muted',
|
|
||||||
'audio-call-speaker','video-call-ringing','video-call-camera-off','video-call-active',
|
|
||||||
'video-call-peer-camera-off','call-minimized',
|
|
||||||
'workbench','organization-directory','meeting-organizer','meeting-attendee',
|
|
||||||
'meeting-create','meeting-detail',
|
|
||||||
'daily-report-editor','daily-report-mine','daily-report-team','daily-report-detail',
|
|
||||||
'monthly-report-editor','monthly-report-list','monthly-report-detail',
|
|
||||||
'executive-mailbox','executive-mailbox-notice',
|
|
||||||
'personal-center','personal-profile','profile-nickname','profile-birth-date',
|
|
||||||
'profile-signature','profile-gender','profile-region','personal-qrcode','android-settings',
|
|
||||||
'cache-clear-confirm','clear-history-confirm','change-password','delete-account-confirm',
|
|
||||||
'app-version','privacy-settings','friend-discovery-settings',
|
|
||||||
'youth-mode-off','youth-mode-on','youth-mode-verify',
|
|
||||||
'notification-permission','permission-settings-reminder','camera-permission-info',
|
|
||||||
'microphone-permission-info',
|
|
||||||
'fraud-risk-warning','fraud-risk-confirmation','abuse-report-reasons',
|
|
||||||
'abuse-report-details','user-feedback',
|
|
||||||
''].includes(hash.path[0])) {
|
|
||||||
newState.sidePanelSelected = hash.path[0];
|
newState.sidePanelSelected = hash.path[0];
|
||||||
newState.sidePanelParams = hash.params;
|
newState.sidePanelParams = hash.params;
|
||||||
} else {
|
} else {
|
||||||
@@ -1011,7 +1022,15 @@ class TinodeWeb extends React.Component {
|
|||||||
// The new bottom nav only has "workbench" and "messages" -- there's no standalone contacts
|
// The new bottom nav only has "workbench" and "messages" -- there's no standalone contacts
|
||||||
// entry point anymore, so landing a fresh login there orphaned the user outside the nav they can
|
// entry point anymore, so landing a fresh login there orphaned the user outside the nav they can
|
||||||
// actually see. Land on the workbench instead (matches the "工作台优先" home screen in the mocks).
|
// actually see. Land on the workbench instead (matches the "工作台优先" home screen in the mocks).
|
||||||
let urlHash = HashNavigation.setUrlSidePanel(window.location.hash, 'workbench');
|
//
|
||||||
|
// 但只在"真的刚走完登录流程"时才这么跳。这个方法在页面刷新触发自动登录时也会执行,原来无条件
|
||||||
|
// 改写 hash,等于把用户刷新前所在的页面冲掉——不管停在个人中心、群设置还是收藏,刷新后一律
|
||||||
|
// 被弹到工作台。现在先看当前 hash 是不是一个有效的、非登录流程的视图,是就原样保留。
|
||||||
|
const currentView = HashNavigation.parseUrlHash(window.location.hash).path[0];
|
||||||
|
const cameFromAuthFlow = !currentView || AUTH_FLOW_VIEWS.includes(currentView) ||
|
||||||
|
!SIDE_PANEL_VIEWS.includes(currentView);
|
||||||
|
let urlHash = cameFromAuthFlow ?
|
||||||
|
HashNavigation.setUrlSidePanel(window.location.hash, 'workbench') : window.location.hash;
|
||||||
if (goToTopic) {
|
if (goToTopic) {
|
||||||
urlHash = HashNavigation.setUrlTopic(urlHash, goToTopic);
|
urlHash = HashNavigation.setUrlTopic(urlHash, goToTopic);
|
||||||
}
|
}
|
||||||
@@ -1739,10 +1758,17 @@ class TinodeWeb extends React.Component {
|
|||||||
handleUpdatePasswordRequest(password) {
|
handleUpdatePasswordRequest(password) {
|
||||||
this.handleError();
|
this.handleError();
|
||||||
|
|
||||||
if (password) {
|
if (!password) {
|
||||||
this.tinode.updateAccountBasic(null, this.tinode.getCurrentLogin(), password)
|
return Promise.reject(new Error('密码不能为空'));
|
||||||
.catch(err => this.handleError(err.message, 'err'));
|
|
||||||
}
|
}
|
||||||
|
// 必须把 Promise 返回出去:调用方(personal-settings-view 的修改密码页)要靠它判断成败。
|
||||||
|
// 原来这里只在内部 catch 掉、什么都不返回,调用方拿到 undefined,于是密码策略被服务端拒绝时
|
||||||
|
// 用户照样看到"密码已更新"并被跳走。跟之前修过的 handleDeleteAccount 是同一类问题。
|
||||||
|
return this.tinode.updateAccountBasic(null, this.tinode.getCurrentLogin(), password)
|
||||||
|
.catch(err => {
|
||||||
|
this.handleError(err.message, 'err');
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChangePermissions(topicName, mode, uid) {
|
handleChangePermissions(topicName, mode, uid) {
|
||||||
|
|||||||
@@ -183,12 +183,17 @@ function ImgFromFile({fileId, className}) {
|
|||||||
// hollow diamond / (expanded) = filled blue diamond, level 3+ (a person, not a sub-department) =
|
// hollow diamond / (expanded) = filled blue diamond, level 3+ (a person, not a sub-department) =
|
||||||
// hollow circle. The backend never returns a depth field (confirmed against organizationDepartmentPayload),
|
// hollow circle. The backend never returns a depth field (confirmed against organizationDepartmentPayload),
|
||||||
// so depth is purely how far this node is from the root in the client's own recursion.
|
// so depth is purely how far this node is from the root in the client's own recursion.
|
||||||
|
// 层级标注统一用中文数字。原来树形分支只手写到"二级"、更深就退化成"3级",
|
||||||
|
// 而搜索分支又硬传字符串 "三",同一个标注两条路径两种格式。
|
||||||
|
const LEVEL_NUMERALS = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
|
||||||
|
const levelLabel = n => LEVEL_NUMERALS[n - 1] || String(n);
|
||||||
|
|
||||||
function DeptRow({dept, depth, expanded, onToggle}) {
|
function DeptRow({dept, depth, expanded, onToggle}) {
|
||||||
const icon = depth === 0 ? 'business' : 'diamond';
|
const icon = depth === 0 ? 'business' : 'diamond';
|
||||||
return <button type="button" className={`organization-department depth-${depth}${expanded ? ' expanded' : ''}`}
|
return <button type="button" className={`organization-department depth-${depth}${expanded ? ' expanded' : ''}`}
|
||||||
style={{paddingLeft: `${1 + depth * 1.1}rem`}} onClick={onToggle}>
|
style={{paddingLeft: `${1 + depth * 1.1}rem`}} onClick={onToggle}>
|
||||||
<Icon>{icon}</Icon>
|
<Icon>{icon}</Icon>
|
||||||
<span><strong>{dept.name}{typeof dept.member_count === 'number' ? `${dept.member_count}人` : ''}({depth === 0 ? '一级' : depth === 1 ? '二级' : `${depth + 1}级`})</strong></span>
|
<span><strong>{dept.name}{typeof dept.member_count === 'number' ? `${dept.member_count}人` : ''}({levelLabel(depth + 1)}级)</strong></span>
|
||||||
<Icon>{expanded ? 'expand_more' : 'chevron_right'}</Icon>
|
<Icon>{expanded ? 'expand_more' : 'chevron_right'}</Icon>
|
||||||
</button>;
|
</button>;
|
||||||
}
|
}
|
||||||
@@ -206,7 +211,7 @@ function PersonRow({person, depth, isMe, level}) {
|
|||||||
return <button type="button" className={`organization-person${isMe ? ' me' : ''}`}
|
return <button type="button" className={`organization-person${isMe ? ' me' : ''}`}
|
||||||
style={{paddingLeft: `${1 + depth * 1.1}rem`}} onClick={() => navigate('group-member-profile', {uid: person.user_id})}>
|
style={{paddingLeft: `${1 + depth * 1.1}rem`}} onClick={() => navigate('group-member-profile', {uid: person.user_id})}>
|
||||||
<Icon>radio_button_unchecked</Icon>
|
<Icon>radio_button_unchecked</Icon>
|
||||||
<span><strong>{person.display_name}({level}级)</strong></span>
|
<span><strong>{person.display_name}({levelLabel(level)}级)</strong></span>
|
||||||
<Icon>chevron_right</Icon>
|
<Icon>chevron_right</Icon>
|
||||||
</button>;
|
</button>;
|
||||||
}
|
}
|
||||||
@@ -390,7 +395,9 @@ function MeetingDetail({hashParams, onToast}) {
|
|||||||
<div><span><b>{counts.accepted}</b><small>已接受</small></span><span><b>{counts.pending}</b><small>待响应</small></span><span><b>{counts.declined}</b><small>已拒绝</small></span></div>
|
<div><span><b>{counts.accepted}</b><small>已接受</small></span><span><b>{counts.pending}</b><small>待响应</small></span><span><b>{counts.declined}</b><small>已拒绝</small></span></div>
|
||||||
<footer>
|
<footer>
|
||||||
{meeting.permissions && meeting.permissions.can_remind ? <button type="button" disabled={busy} onClick={remind}><Icon>notifications</Icon>提醒未响应</button> : null}
|
{meeting.permissions && meeting.permissions.can_remind ? <button type="button" disabled={busy} onClick={remind}><Icon>notifications</Icon>提醒未响应</button> : null}
|
||||||
{meeting.permissions && meeting.permissions.can_cancel && meeting.state !== 'cancelled' ? <button type="button" className="danger" disabled={busy} onClick={cancel}>取消会议</button> : null}
|
{/* 后端常量是 "canceled" 单 l(store/types/meetings.go:7),原来写成 "cancelled" 永远不相等,
|
||||||
|
会议取消之后"取消会议"按钮还挂在那儿 */}
|
||||||
|
{meeting.permissions && meeting.permissions.can_cancel && meeting.state !== 'canceled' ? <button type="button" className="danger" disabled={busy} onClick={cancel}>取消会议</button> : null}
|
||||||
</footer>
|
</footer>
|
||||||
</section> : null}
|
</section> : null}
|
||||||
</main>
|
</main>
|
||||||
@@ -503,7 +510,7 @@ function OrganizationDirectory({myUserId}) {
|
|||||||
{searchResults.length === 0 ? <p className="organization-loading">未找到匹配的成员</p> :
|
{searchResults.length === 0 ? <p className="organization-loading">未找到匹配的成员</p> :
|
||||||
searchResults.map((item, idx) => item.type === 'department' ?
|
searchResults.map((item, idx) => item.type === 'department' ?
|
||||||
<SearchDeptRow key={'d' + (item.department && item.department.department_id || idx)} dept={item.department} /> :
|
<SearchDeptRow key={'d' + (item.department && item.department.department_id || idx)} dept={item.department} /> :
|
||||||
<PersonRow key={'u' + (item.user && item.user.user_id || idx)} person={item.user} depth={0} level="三"
|
<PersonRow key={'u' + (item.user && item.user.user_id || idx)} person={item.user} depth={0} level={3}
|
||||||
isMe={item.user && (item.user.user_id === myUserId || 'usr' + item.user.user_id === myUserId)} />)}
|
isMe={item.user && (item.user.user_id === myUserId || 'usr' + item.user.user_id === myUserId)} />)}
|
||||||
</section> :
|
</section> :
|
||||||
<section className="organization-tree">
|
<section className="organization-tree">
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ export default class MetaMessage extends React.PureComponent {
|
|||||||
// A date badge.
|
// A date badge.
|
||||||
content = <>{this.props.date}</>
|
content = <>{this.props.date}</>
|
||||||
bubbleClass += ' date';
|
bubbleClass += ' date';
|
||||||
|
} else if (this.props.deleted) {
|
||||||
|
// 撤回/删除占位。messages-view.jsx 对 msg.hi 的消息一直在传 deleted={true},但这里从来
|
||||||
|
// 没有处理过这个分支,于是整个组件走到下面的 `return null` —— 撤回一条消息之后时间线上
|
||||||
|
// 什么都不剩,看不出这里曾经有过一条消息。文案复用已有的 message_deleted("消息已删除")。
|
||||||
|
content = <i className="gray">{this.props.deletedText || '消息已删除'}</i>;
|
||||||
|
bubbleClass += ' date';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
|
|||||||
+97
-37
@@ -6638,11 +6638,26 @@ function IndependentPassword({
|
|||||||
}
|
}
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
await (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_2__.setIndependentPassword)(pin, 'youth_mode', actionToken);
|
const result = await (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_2__.setIndependentPassword)(pin, 'youth_mode', actionToken);
|
||||||
sessionStorage.removeItem('im-action-token:' + IDENTITY_CHECK_SCENE);
|
sessionStorage.removeItem('im-action-token:' + IDENTITY_CHECK_SCENE);
|
||||||
|
setPin('');
|
||||||
|
const enableToken = result && result.enable_action_token;
|
||||||
|
if (enableToken) {
|
||||||
|
try {
|
||||||
|
const current = await (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_2__.getYouthMode)();
|
||||||
|
await (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_2__.enableYouthMode)(current.policy_version, enableToken);
|
||||||
|
setError('');
|
||||||
|
setToast('青少年模式已开启');
|
||||||
|
window.setTimeout(() => navigate('youth-mode-on'), 600);
|
||||||
|
return;
|
||||||
|
} catch (enableErr) {
|
||||||
|
setError('独立密码已设置,但自动开启未成功:' + (enableErr.message || '请返回重试'));
|
||||||
|
window.setTimeout(() => navigate('youth-mode-off'), 1200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
setError('');
|
setError('');
|
||||||
setToast('独立密码设置成功');
|
setToast('独立密码设置成功');
|
||||||
setPin('');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || '设置失败,请返回重新验证身份');
|
setError(err.message || '设置失败,请返回重新验证身份');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -7690,6 +7705,11 @@ function FriendRequests({
|
|||||||
}, void 0, true)]
|
}, void 0, true)]
|
||||||
}, void 0, true);
|
}, void 0, true);
|
||||||
}
|
}
|
||||||
|
const GENDER_LABELS = {
|
||||||
|
male: '男',
|
||||||
|
female: '女',
|
||||||
|
other: '其他'
|
||||||
|
};
|
||||||
function seedProfile(item) {
|
function seedProfile(item) {
|
||||||
if (!item || !item.user_id) return;
|
if (!item || !item.user_id) return;
|
||||||
(0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.writeCachedData)('profile-seed:' + item.user_id, {
|
(0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.writeCachedData)('profile-seed:' + item.user_id, {
|
||||||
@@ -8035,7 +8055,7 @@ function Profile({
|
|||||||
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("dt", {
|
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("dt", {
|
||||||
children: "性别"
|
children: "性别"
|
||||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("dd", {
|
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("dd", {
|
||||||
children: profile.gender
|
children: GENDER_LABELS[profile.gender] || profile.gender
|
||||||
}, void 0, false)]
|
}, void 0, false)]
|
||||||
}, void 0, true) : null, (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("div", {
|
}, void 0, true) : null, (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("div", {
|
||||||
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("dt", {
|
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("dt", {
|
||||||
@@ -8684,12 +8704,17 @@ function Search({
|
|||||||
function draftyContentOf(content) {
|
function draftyContentOf(content) {
|
||||||
return tinode_sdk__WEBPACK_IMPORTED_MODULE_1__.Drafty.isValid(content) ? content : null;
|
return tinode_sdk__WEBPACK_IMPORTED_MODULE_1__.Drafty.isValid(content) ? content : null;
|
||||||
}
|
}
|
||||||
|
const MEDIA_ENTITY_TYPES = ['IM', 'VD', 'AU', 'EX'];
|
||||||
function firstAttachment(content) {
|
function firstAttachment(content) {
|
||||||
const drafty = draftyContentOf(content);
|
const drafty = draftyContentOf(content);
|
||||||
if (!drafty) return null;
|
if (!drafty) return null;
|
||||||
let found = null;
|
let found = null;
|
||||||
tinode_sdk__WEBPACK_IMPORTED_MODULE_1__.Drafty.attachments(drafty, att => {
|
tinode_sdk__WEBPACK_IMPORTED_MODULE_1__.Drafty.entities(drafty, (data, _index, tp) => {
|
||||||
if (!found) found = att;
|
if (!found && data && MEDIA_ENTITY_TYPES.includes(tp)) {
|
||||||
|
found = data;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
@@ -9306,18 +9331,23 @@ function DownloadPage({
|
|||||||
}
|
}
|
||||||
function Highlight({
|
function Highlight({
|
||||||
text,
|
text,
|
||||||
ranges
|
query
|
||||||
}) {
|
}) {
|
||||||
if (!ranges || !ranges.length) return text;
|
const needle = (query || '').trim();
|
||||||
|
if (!needle || !text) return text;
|
||||||
const parts = [];
|
const parts = [];
|
||||||
|
const haystack = text.toLowerCase();
|
||||||
|
const lowered = needle.toLowerCase();
|
||||||
let last = 0;
|
let last = 0;
|
||||||
ranges.forEach(([start, len], i) => {
|
for (let at = haystack.indexOf(lowered); at !== -1; at = haystack.indexOf(lowered, at + lowered.length)) {
|
||||||
parts.push(text.slice(last, start), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_8__.jsxDEV)("mark", {
|
if (at > last) parts.push(text.slice(last, at));
|
||||||
children: text.slice(start, start + len)
|
parts.push((0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_8__.jsxDEV)("mark", {
|
||||||
}, i, false));
|
children: text.slice(at, at + needle.length)
|
||||||
last = start + len;
|
}, at, false));
|
||||||
});
|
last = at + needle.length;
|
||||||
parts.push(text.slice(last));
|
}
|
||||||
|
if (!parts.length) return text;
|
||||||
|
if (last < text.length) parts.push(text.slice(last));
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
function HistoryNav({
|
function HistoryNav({
|
||||||
@@ -9468,7 +9498,7 @@ function History({
|
|||||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_8__.jsxDEV)("p", {
|
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_8__.jsxDEV)("p", {
|
||||||
children: item.type === 'text' ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_8__.jsxDEV)(Highlight, {
|
children: item.type === 'text' ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_8__.jsxDEV)(Highlight, {
|
||||||
text: item.snippet || '',
|
text: item.snippet || '',
|
||||||
ranges: item.matches
|
query: query
|
||||||
}, void 0, false) : TYPE_LABEL[item.type] || item.snippet || ''
|
}, void 0, false) : TYPE_LABEL[item.type] || item.snippet || ''
|
||||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_8__.jsxDEV)("small", {
|
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_8__.jsxDEV)("small", {
|
||||||
children: (0,_lib_strformat_js__WEBPACK_IMPORTED_MODULE_4__.shortDateFormat)(new Date(item.ts))
|
children: (0,_lib_strformat_js__WEBPACK_IMPORTED_MODULE_4__.shortDateFormat)(new Date(item.ts))
|
||||||
@@ -11148,7 +11178,13 @@ function GroupSettings({
|
|||||||
}, void 0, false)]
|
}, void 0, false)]
|
||||||
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("button", {
|
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
onClick: () => onToast('举报入口暂未开放'),
|
onClick: () => {
|
||||||
|
let hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel('', 'abuse-report-reasons');
|
||||||
|
hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(hash, 'target_type', 'group');
|
||||||
|
hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(hash, 'target_id', topic);
|
||||||
|
hash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(hash, 'target_name', group && group.name || topic);
|
||||||
|
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].navigateTo(hash);
|
||||||
|
},
|
||||||
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("span", {
|
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("span", {
|
||||||
children: "投诉/举报"
|
children: "投诉/举报"
|
||||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Icon, {
|
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Icon, {
|
||||||
@@ -11696,8 +11732,14 @@ function MemberPicker({
|
|||||||
user_id
|
user_id
|
||||||
}))) : (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.addGroupAdmins)(topic, selected);
|
}))) : (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.addGroupAdmins)(topic, selected);
|
||||||
call.then(result => {
|
call.then(result => {
|
||||||
const failed = result && result.succeeded ? result.succeeded.filter(s => !s.ok) : [];
|
let failedCount;
|
||||||
setToast(failed.length ? `成功设置 ${selected.length - failed.length} 人,${failed.length} 人设置失败` : '设置成功');
|
if (mute) {
|
||||||
|
const done = result && Array.isArray(result.items) ? result.items.length : selected.length;
|
||||||
|
failedCount = Math.max(selected.length - done, 0);
|
||||||
|
} else {
|
||||||
|
failedCount = result && Array.isArray(result.succeeded) ? result.succeeded.filter(s => !s.ok).length : 0;
|
||||||
|
}
|
||||||
|
setToast(failedCount ? `成功设置 ${selected.length - failedCount} 人,${failedCount} 人设置失败` : '设置成功');
|
||||||
window.setTimeout(() => navigate(back, topic), 900);
|
window.setTimeout(() => navigate(back, topic), 900);
|
||||||
}).catch(err => onToast(err.message || '设置失败')).finally(() => setBusy(false));
|
}).catch(err => onToast(err.message || '设置失败')).finally(() => setBusy(false));
|
||||||
};
|
};
|
||||||
@@ -12176,11 +12218,11 @@ function GroupDescriptionEditor({
|
|||||||
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("textarea", {
|
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("textarea", {
|
||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
value: value,
|
value: value,
|
||||||
maxLength: 225,
|
maxLength: 200,
|
||||||
placeholder: "请输入群简介内容",
|
placeholder: "请输入群简介内容",
|
||||||
onChange: e => setValue(e.target.value)
|
onChange: e => setValue(e.target.value)
|
||||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("small", {
|
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("small", {
|
||||||
children: [value.length, "/225"]
|
children: [value.length, "/200"]
|
||||||
}, void 0, true)]
|
}, void 0, true)]
|
||||||
}, void 0, true)]
|
}, void 0, true)]
|
||||||
}, void 0, true);
|
}, void 0, true);
|
||||||
@@ -12660,6 +12702,7 @@ function normalizeConversations(chatList, myUserId) {
|
|||||||
summary,
|
summary,
|
||||||
time: formatConversationTime(item.touched),
|
time: formatConversationTime(item.touched),
|
||||||
unread: Math.max(Number(item.unread) || 0, 0),
|
unread: Math.max(Number(item.unread) || 0, 0),
|
||||||
|
avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_4__.makeImageUrl)(item.public && item.public.photo),
|
||||||
icon: isGroup ? 'group' : 'person',
|
icon: isGroup ? 'group' : 'person',
|
||||||
online: isGroup ? undefined : !!item.online,
|
online: isGroup ? undefined : !!item.online,
|
||||||
muted: !!(item.acs && item.acs.isMuted && item.acs.isMuted()),
|
muted: !!(item.acs && item.acs.isMuted && item.acs.isMuted()),
|
||||||
@@ -12983,11 +13026,11 @@ const NOTIFICATION_TEXT = {
|
|||||||
accepted: '对方已通过你的好友申请',
|
accepted: '对方已通过你的好友申请',
|
||||||
rejected: '对方拒绝了你的好友申请'
|
rejected: '对方拒绝了你的好友申请'
|
||||||
};
|
};
|
||||||
const MACHINE_CODE = /^[a-z0-9._-]+$/;
|
|
||||||
function systemNotificationText(item) {
|
function systemNotificationText(item) {
|
||||||
const summary = (item.summary || '').trim();
|
const summary = (item.summary || '').trim();
|
||||||
if (summary && !MACHINE_CODE.test(summary)) return summary;
|
const mapped = NOTIFICATION_TEXT[summary] || NOTIFICATION_TEXT[item.event_type];
|
||||||
return NOTIFICATION_TEXT[summary] || NOTIFICATION_TEXT[item.event_type] || '您有一条新的系统通知。';
|
if (mapped) return mapped;
|
||||||
|
return summary || '您有一条新的系统通知。';
|
||||||
}
|
}
|
||||||
function SystemMessages({
|
function SystemMessages({
|
||||||
authenticated
|
authenticated
|
||||||
@@ -15009,6 +15052,7 @@ function ChangePassword({
|
|||||||
const [oldPwd, setOldPwd] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
|
const [oldPwd, setOldPwd] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
|
||||||
const [newPwd, setNewPwd] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
|
const [newPwd, setNewPwd] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
|
||||||
const [confirmPwd, setConfirmPwd] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
|
const [confirmPwd, setConfirmPwd] = react__WEBPACK_IMPORTED_MODULE_0___default().useState('');
|
||||||
|
const [busy, setBusy] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
if (!oldPwd || newPwd.length < 6) {
|
if (!oldPwd || newPwd.length < 6) {
|
||||||
onToast('请输入原密码,新密码至少6位');
|
onToast('请输入原密码,新密码至少6位');
|
||||||
@@ -15022,9 +15066,15 @@ function ChangePassword({
|
|||||||
onToast('当前环境不支持修改密码');
|
onToast('当前环境不支持修改密码');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onUpdatePassword(newPwd);
|
if (busy) return;
|
||||||
onToast('密码已更新');
|
setBusy(true);
|
||||||
window.setTimeout(() => navigate('android-settings'), 500);
|
Promise.resolve(onUpdatePassword(newPwd)).then(() => {
|
||||||
|
onToast('密码已更新');
|
||||||
|
window.setTimeout(() => navigate('android-settings'), 500);
|
||||||
|
}).catch(err => {
|
||||||
|
onToast(err && err.message || '修改失败,请重试');
|
||||||
|
setBusy(false);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
return (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("div", {
|
return (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_7__.jsxDEV)("div", {
|
||||||
className: "personal-page profile-editor-page",
|
className: "personal-page profile-editor-page",
|
||||||
@@ -15717,11 +15767,11 @@ function FraudAwareness({
|
|||||||
title: "安全提醒",
|
title: "安全提醒",
|
||||||
back: secondary ? 'fraud-risk-warning' : 'personal-center'
|
back: secondary ? 'fraud-risk-warning' : 'personal-center'
|
||||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_4__.jsxDEV)("main", {
|
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_4__.jsxDEV)("main", {
|
||||||
className: "risk-info-page",
|
className: "risk-info-page risk-dialog",
|
||||||
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_4__.jsxDEV)("span", {
|
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_4__.jsxDEV)("span", {
|
||||||
className: "risk-symbol",
|
className: "risk-symbol",
|
||||||
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_4__.jsxDEV)(Icon, {
|
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_4__.jsxDEV)(Icon, {
|
||||||
children: secondary ? 'warning_amber' : 'shield_lock'
|
children: secondary ? 'warning_amber' : 'verified_user'
|
||||||
}, void 0, false)
|
}, void 0, false)
|
||||||
}, void 0, false), secondary ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_4__.jsxDEV)(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_4__.Fragment, {
|
}, void 0, false), secondary ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_4__.jsxDEV)(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_4__.Fragment, {
|
||||||
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_4__.jsxDEV)("p", {
|
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_4__.jsxDEV)("p", {
|
||||||
@@ -17035,6 +17085,8 @@ const messages = (0,react_intl__WEBPACK_IMPORTED_MODULE_1__.defineMessages)({
|
|||||||
description: 'Placeholder for the country selector'
|
description: 'Placeholder for the country selector'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const SIDE_PANEL_VIEWS = ['acc_general', 'archive', 'blocked', 'contacts', 'cred', 'crop', 'edit', 'general', 'newtpk', 'notif', 'register', 'reset', 'security', 'settings', 'support', 'wallpapers', 'tenant', 'login', 'enterprise-register', 'enterprise-feedback', 'identity-check', 'realname', 'realname-verified', 'realname-reminder', 'independent-password', 'messages-home', 'messages-offline', 'messages-menu', 'message-actions', 'system-messages', 'chat-inbound-request', 'chat-inbound-message', 'chat-outbound-request', 'chat-status', 'chat-friend', 'chat-media', 'chat-blocked', 'chat-deleted', 'chat-standard', 'chat-attachments', 'chat-mixed', 'chat-contact-card', 'compose-keyboard', 'mentions-select', 'mentions-complete', 'mentions-message', 'emoji-panel', 'emoji-message', 'voice-ready', 'voice-hold', 'voice-recording', 'media-uploading', 'media-gallery', 'media-retry', 'upload-file', 'file-message', 'message-multiselect', 'favorite-success', 'forward-select', 'forward-multiple', 'forward-individual', 'forward-bundle', 'address-book', 'friend-requests', 'add-friend', 'my-friend-qr', 'friend-search-result', 'friend-verification', 'friend-profile', 'group-member-profile', 'friend-settings', 'friend-settings-blocked', 'friend-delete-confirm', 'block-list', 'block-confirm', 'clear-chat-confirm', 'group-list', 'group-empty', 'group-invite', 'group-events', 'group-join-requests', 'group-new-request', 'group-announcement', 'group-summary', 'group-announcement-detail', 'group-settings', 'group-profile', 'group-avatar', 'group-nickname', 'group-description', 'group-my-nickname', 'group-role-message', 'group-qr', 'group-governance', 'group-admins', 'group-admin-add', 'group-admin-success', 'group-admin-partial', 'group-admin-removed', 'group-mute-add', 'group-muted', 'group-owner-select', 'group-owner-confirm', 'group-files', 'group-file-upload', 'favorites', 'favorites-mixed', 'favorites-files', 'favorites-text', 'favorites-voice', 'favorites-links', 'favorites-select', 'favorite-share', 'file-download', 'file-downloading', 'file-downloaded', 'history-search', 'history-files', 'history-links', 'history-images', 'history-videos', 'call-history', 'call-incoming', 'audio-call-ringing', 'audio-call-active', 'audio-call-muted', 'audio-call-speaker', 'video-call-ringing', 'video-call-camera-off', 'video-call-active', 'video-call-peer-camera-off', 'call-minimized', 'workbench', 'organization-directory', 'meeting-organizer', 'meeting-attendee', 'meeting-create', 'meeting-detail', 'daily-report-editor', 'daily-report-mine', 'daily-report-team', 'daily-report-detail', 'monthly-report-editor', 'monthly-report-list', 'monthly-report-detail', 'executive-mailbox', 'executive-mailbox-notice', 'personal-center', 'personal-profile', 'profile-nickname', 'profile-birth-date', 'profile-signature', 'profile-gender', 'profile-region', 'personal-qrcode', 'android-settings', 'cache-clear-confirm', 'clear-history-confirm', 'change-password', 'delete-account-confirm', 'app-version', 'privacy-settings', 'friend-discovery-settings', 'youth-mode-off', 'youth-mode-on', 'youth-mode-verify', 'notification-permission', 'permission-settings-reminder', 'camera-permission-info', 'microphone-permission-info', 'fraud-risk-warning', 'fraud-risk-confirmation', 'abuse-report-reasons', 'abuse-report-details', 'user-feedback', ''];
|
||||||
|
const AUTH_FLOW_VIEWS = ['tenant', 'login', 'register', 'reset', 'cred', 'enterprise-register', 'enterprise-feedback'];
|
||||||
class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component) {
|
class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component) {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -17283,7 +17335,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const parsedNav = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].parseUrlHash(window.location.hash);
|
const parsedNav = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].parseUrlHash(window.location.hash);
|
||||||
if (!['cred', 'reset', 'register'].includes(parsedNav.path[0])) {
|
if (!SIDE_PANEL_VIEWS.includes(parsedNav.path[0]) || !parsedNav.path[0]) {
|
||||||
this.setState({
|
this.setState({
|
||||||
requestedTopic: parsedNav.path[1]
|
requestedTopic: parsedNav.path[1]
|
||||||
});
|
});
|
||||||
@@ -17494,7 +17546,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
|||||||
newTopicTabSelected: hash.params.tab
|
newTopicTabSelected: hash.params.tab
|
||||||
};
|
};
|
||||||
if (hash.path && hash.path.length > 0) {
|
if (hash.path && hash.path.length > 0) {
|
||||||
if (['acc_general', 'archive', 'blocked', 'contacts', 'cred', 'crop', 'edit', 'general', 'newtpk', 'notif', 'register', 'reset', 'security', 'settings', 'support', 'wallpapers', 'tenant', 'login', 'enterprise-register', 'enterprise-feedback', 'identity-check', 'realname', 'realname-verified', 'realname-reminder', 'independent-password', 'messages-home', 'messages-offline', 'messages-menu', 'message-actions', 'system-messages', 'chat-inbound-request', 'chat-inbound-message', 'chat-outbound-request', 'chat-status', 'chat-friend', 'chat-media', 'chat-blocked', 'chat-deleted', 'chat-standard', 'chat-attachments', 'chat-mixed', 'chat-contact-card', 'compose-keyboard', 'mentions-select', 'mentions-complete', 'mentions-message', 'emoji-panel', 'emoji-message', 'voice-ready', 'voice-hold', 'voice-recording', 'media-uploading', 'media-gallery', 'media-retry', 'upload-file', 'file-message', 'message-multiselect', 'favorite-success', 'forward-select', 'forward-multiple', 'forward-individual', 'forward-bundle', 'address-book', 'friend-requests', 'add-friend', 'my-friend-qr', 'friend-search-result', 'friend-verification', 'friend-profile', 'group-member-profile', 'friend-settings', 'friend-settings-blocked', 'friend-delete-confirm', 'block-list', 'block-confirm', 'clear-chat-confirm', 'group-list', 'group-empty', 'group-invite', 'group-events', 'group-join-requests', 'group-new-request', 'group-announcement', 'group-summary', 'group-announcement-detail', 'group-settings', 'group-profile', 'group-avatar', 'group-nickname', 'group-description', 'group-my-nickname', 'group-role-message', 'group-qr', 'group-governance', 'group-admins', 'group-admin-add', 'group-admin-success', 'group-admin-partial', 'group-admin-removed', 'group-mute-add', 'group-muted', 'group-owner-select', 'group-owner-confirm', 'group-files', 'group-file-upload', 'favorites', 'favorites-mixed', 'favorites-files', 'favorites-text', 'favorites-voice', 'favorites-links', 'favorites-select', 'favorite-share', 'file-download', 'file-downloading', 'file-downloaded', 'history-search', 'history-files', 'history-links', 'history-images', 'history-videos', 'call-history', 'call-incoming', 'audio-call-ringing', 'audio-call-active', 'audio-call-muted', 'audio-call-speaker', 'video-call-ringing', 'video-call-camera-off', 'video-call-active', 'video-call-peer-camera-off', 'call-minimized', 'workbench', 'organization-directory', 'meeting-organizer', 'meeting-attendee', 'meeting-create', 'meeting-detail', 'daily-report-editor', 'daily-report-mine', 'daily-report-team', 'daily-report-detail', 'monthly-report-editor', 'monthly-report-list', 'monthly-report-detail', 'executive-mailbox', 'executive-mailbox-notice', 'personal-center', 'personal-profile', 'profile-nickname', 'profile-birth-date', 'profile-signature', 'profile-gender', 'profile-region', 'personal-qrcode', 'android-settings', 'cache-clear-confirm', 'clear-history-confirm', 'change-password', 'delete-account-confirm', 'app-version', 'privacy-settings', 'friend-discovery-settings', 'youth-mode-off', 'youth-mode-on', 'youth-mode-verify', 'notification-permission', 'permission-settings-reminder', 'camera-permission-info', 'microphone-permission-info', 'fraud-risk-warning', 'fraud-risk-confirmation', 'abuse-report-reasons', 'abuse-report-details', 'user-feedback', ''].includes(hash.path[0])) {
|
if (SIDE_PANEL_VIEWS.includes(hash.path[0])) {
|
||||||
newState.sidePanelSelected = hash.path[0];
|
newState.sidePanelSelected = hash.path[0];
|
||||||
newState.sidePanelParams = hash.params;
|
newState.sidePanelParams = hash.params;
|
||||||
} else {
|
} else {
|
||||||
@@ -17766,7 +17818,9 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
|||||||
loadSpinnerVisible: false
|
loadSpinnerVisible: false
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
let urlHash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(window.location.hash, 'workbench');
|
const currentView = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].parseUrlHash(window.location.hash).path[0];
|
||||||
|
const cameFromAuthFlow = !currentView || AUTH_FLOW_VIEWS.includes(currentView) || !SIDE_PANEL_VIEWS.includes(currentView);
|
||||||
|
let urlHash = cameFromAuthFlow ? _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlSidePanel(window.location.hash, 'workbench') : window.location.hash;
|
||||||
if (goToTopic) {
|
if (goToTopic) {
|
||||||
urlHash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlTopic(urlHash, goToTopic);
|
urlHash = _lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].setUrlTopic(urlHash, goToTopic);
|
||||||
}
|
}
|
||||||
@@ -18413,9 +18467,13 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
|||||||
}
|
}
|
||||||
handleUpdatePasswordRequest(password) {
|
handleUpdatePasswordRequest(password) {
|
||||||
this.handleError();
|
this.handleError();
|
||||||
if (password) {
|
if (!password) {
|
||||||
this.tinode.updateAccountBasic(null, this.tinode.getCurrentLogin(), password).catch(err => this.handleError(err.message, 'err'));
|
return Promise.reject(new Error('密码不能为空'));
|
||||||
}
|
}
|
||||||
|
return this.tinode.updateAccountBasic(null, this.tinode.getCurrentLogin(), password).catch(err => {
|
||||||
|
this.handleError(err.message, 'err');
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
handleChangePermissions(topicName, mode, uid) {
|
handleChangePermissions(topicName, mode, uid) {
|
||||||
const topic = this.tinode.getTopic(topicName);
|
const topic = this.tinode.getTopic(topicName);
|
||||||
@@ -19855,6 +19913,8 @@ function ImgFromFile({
|
|||||||
className: className
|
className: className
|
||||||
}, void 0, false);
|
}, void 0, false);
|
||||||
}
|
}
|
||||||
|
const LEVEL_NUMERALS = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
|
||||||
|
const levelLabel = n => LEVEL_NUMERALS[n - 1] || String(n);
|
||||||
function DeptRow({
|
function DeptRow({
|
||||||
dept,
|
dept,
|
||||||
depth,
|
depth,
|
||||||
@@ -19873,7 +19933,7 @@ function DeptRow({
|
|||||||
children: icon
|
children: icon
|
||||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("span", {
|
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("span", {
|
||||||
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("strong", {
|
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("strong", {
|
||||||
children: [dept.name, typeof dept.member_count === 'number' ? `${dept.member_count}人` : '', "(", depth === 0 ? '一级' : depth === 1 ? '二级' : `${depth + 1}级`, ")"]
|
children: [dept.name, typeof dept.member_count === 'number' ? `${dept.member_count}人` : '', "(", levelLabel(depth + 1), "级)"]
|
||||||
}, void 0, true)
|
}, void 0, true)
|
||||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)(Icon, {
|
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)(Icon, {
|
||||||
children: expanded ? 'expand_more' : 'chevron_right'
|
children: expanded ? 'expand_more' : 'chevron_right'
|
||||||
@@ -19916,7 +19976,7 @@ function PersonRow({
|
|||||||
children: "radio_button_unchecked"
|
children: "radio_button_unchecked"
|
||||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("span", {
|
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("span", {
|
||||||
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("strong", {
|
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("strong", {
|
||||||
children: [person.display_name, "(", level, "级)"]
|
children: [person.display_name, "(", levelLabel(level), "级)"]
|
||||||
}, void 0, true)
|
}, void 0, true)
|
||||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)(Icon, {
|
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)(Icon, {
|
||||||
children: "chevron_right"
|
children: "chevron_right"
|
||||||
@@ -20280,7 +20340,7 @@ function MeetingDetail({
|
|||||||
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)(Icon, {
|
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)(Icon, {
|
||||||
children: "notifications"
|
children: "notifications"
|
||||||
}, void 0, false), "提醒未响应"]
|
}, void 0, false), "提醒未响应"]
|
||||||
}, void 0, true) : null, meeting.permissions && meeting.permissions.can_cancel && meeting.state !== 'cancelled' ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("button", {
|
}, void 0, true) : null, meeting.permissions && meeting.permissions.can_cancel && meeting.state !== 'canceled' ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
className: "danger",
|
className: "danger",
|
||||||
disabled: busy,
|
disabled: busy,
|
||||||
@@ -20511,7 +20571,7 @@ function OrganizationDirectory({
|
|||||||
}, 'd' + (item.department && item.department.department_id || idx), false) : (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)(PersonRow, {
|
}, 'd' + (item.department && item.department.department_id || idx), false) : (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)(PersonRow, {
|
||||||
person: item.user,
|
person: item.user,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
level: "三",
|
level: 3,
|
||||||
isMe: item.user && (item.user.user_id === myUserId || 'usr' + item.user.user_id === myUserId)
|
isMe: item.user && (item.user.user_id === myUserId || 'usr' + item.user.user_id === myUserId)
|
||||||
}, 'u' + (item.user && item.user.user_id || idx), false))
|
}, 'u' + (item.user && item.user.user_id || idx), false))
|
||||||
}, void 0, false) : (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("section", {
|
}, void 0, false) : (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__.jsxDEV)("section", {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -125,6 +125,11 @@ const NOTIFICATION_EXEC_INTERVAL = 300;
|
|||||||
const SHOW_GO_TO_LAST_DIST = 100;
|
const SHOW_GO_TO_LAST_DIST = 100;
|
||||||
const FETCH_PAGE_TRIGGER = 40;
|
const FETCH_PAGE_TRIGGER = 40;
|
||||||
const messages = (0,react_intl__WEBPACK_IMPORTED_MODULE_1__.defineMessages)({
|
const messages = (0,react_intl__WEBPACK_IMPORTED_MODULE_1__.defineMessages)({
|
||||||
|
message_deleted: {
|
||||||
|
id: 'message_deleted',
|
||||||
|
defaultMessage: 'message deleted',
|
||||||
|
description: 'Placeholder shown in the timeline where a message was recalled or deleted'
|
||||||
|
},
|
||||||
online_now: {
|
online_now: {
|
||||||
id: 'online_now',
|
id: 'online_now',
|
||||||
defaultMessage: 'online now',
|
defaultMessage: 'online now',
|
||||||
@@ -1840,7 +1845,8 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
|
|||||||
}
|
}
|
||||||
if (msg.hi) {
|
if (msg.hi) {
|
||||||
messageNodes.push((0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_22__.jsxDEV)(_widgets_meta_message_jsx__WEBPACK_IMPORTED_MODULE_11__["default"], {
|
messageNodes.push((0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_22__.jsxDEV)(_widgets_meta_message_jsx__WEBPACK_IMPORTED_MODULE_11__["default"], {
|
||||||
deleted: true
|
deleted: true,
|
||||||
|
deletedText: this.props.intl.formatMessage(messages.message_deleted)
|
||||||
}, msg.seq, false));
|
}, msg.seq, false));
|
||||||
} else {
|
} else {
|
||||||
const thisDate = new Date(msg.ts);
|
const thisDate = new Date(msg.ts);
|
||||||
@@ -2837,6 +2843,12 @@ class MetaMessage extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComp
|
|||||||
children: this.props.date
|
children: this.props.date
|
||||||
}, void 0, false);
|
}, void 0, false);
|
||||||
bubbleClass += ' date';
|
bubbleClass += ' date';
|
||||||
|
} else if (this.props.deleted) {
|
||||||
|
content = (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_1__.jsxDEV)("i", {
|
||||||
|
className: "gray",
|
||||||
|
children: this.props.deletedText || '消息已删除'
|
||||||
|
}, void 0, false);
|
||||||
|
bubbleClass += ' date';
|
||||||
}
|
}
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_1__.jsxDEV)(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_1__.Fragment, {
|
return (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_1__.jsxDEV)(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_1__.Fragment, {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user