修复全页面走查发现的问题(第二批:登录、通讯录、群聊)
第1章
- tinode-web: 手输密码登录失败会 navigateTo('') 把用户弹回企业码页,刚输的账号丢了,而
TenantEntry 不渲染 FormAlert,那句错误提示根本看不到。改成只有存储的 token 失效才回登录入口。
- auth-onboarding IdentityCheck: 限流 429 时服务端会连同未过期的 challenge_id 一起返回,
原来只显示错误文案、challengeId 留空,于是 submit 开头的守卫静默吞掉点击——输完验证码点
"确定"毫无反应也无提示。同文件 VerificationButton 早就处理对了,这里是漏抄。
- auth-onboarding Register: 手机号注册路径硬传 invite_code:'',把用户填的邀请码丢了。后端
user.go:175-181 对邀请码的校验和 RecordUse 不区分注册路径,UI 上也摆着这个输入框。
第5章
- 好友列表主副行显示同一串字:后端在有备注时把 display_name 也设成备注(im_http.go:8500-8507),
两个字段同值。加"不同才显示"的守卫。
- 拉黑确认弹窗把设置页当背景渲染并摆出"操作之后"的样子,但背景组件自己的 getUserProfile
回来后又把开关改回未拉黑、"删除好友"红条重新冒出来。加 pinBlocked 挡住这次覆盖。
第6章
- 群列表把群名显示成 topic id、成员数恒为 0。根因在后端:handler 用 store.Users.GetSubs,而
store 自己的注释就写着它 "Does not load Public/Trusted or Private"(store.go:834-835),
groupListItemPayload 却去读 sub.GetPublic()/GetSubCnt()(应该用 GetTopics)。已记为后端问题;
前端先用本地 Tinode 会话缓存兜底(消息首页一直这么取,是准的)。
- 禁言页每行头像画成群组图标(Avatar 兜底图标默认 'group',没传 icon="person"),
且顺带从成员表带上真实头像。
- 禁言页"以上成员不允许发言"被当成空状态用,语义正好反了——有禁言成员时这句消失、
一个都没有时反而显示。改为列表下方常驻注脚,空态另给文案,并补 .group-inline-note 样式。
- bytesToGB 只输出到 MB,1.4GB 的文件显示成 "1433.6 MB"。
- 群文件"来自"直接显示裸 uploader_id(groupFilePayload 不给昵称),拉成员表交叉引用。
- 群公告从聊天页横幅进入后返回,会把会话一起关掉:navigate 只写 query 参数,path[1] 仍为空,
handleHashRoute 就把 topicSelected 清成 null。Header 支持 onBack,显式把 topic 写回 path[1]。
- 群成员点头像打不开非好友资料页:目标页先读 'profile-seed:' 缓存,因为 getUserProfile 对
非好友直接 403。第5章入口一直有 seed,这里漏了。
- 群简介上限 225 超过后端硬限 200(im_http.go:4667),写满必被打回。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5a07bb4e3b
commit
02b98cceae
@@ -10126,6 +10126,15 @@ div.toast.show {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 列表下方的常驻灰色注脚(禁言页的"以上成员不允许发言"),区别于上面的空状态文案。 */
|
||||
.group-inline-note {
|
||||
margin: 0;
|
||||
padding: 0.9rem 1rem 1.4rem;
|
||||
color: #969da5;
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Alphabet-group section header inside MemberPicker/OwnerSelect, same treatment as .group-admin-page h2. */
|
||||
.group-section-title {
|
||||
margin: 0;
|
||||
|
||||
@@ -422,7 +422,11 @@ function Register({errorText, errorLevel, onImRegisterRequest}) {
|
||||
// set a nickname afterwards).
|
||||
const secretAccount = form.username || ('+86' + form.phone);
|
||||
onImRegisterRequest(secretAccount, form.password, form.username || '',
|
||||
{username: form.username || '', phone: form.phone, invite_code: ''},
|
||||
// 邀请码不能在手机号路径上丢掉:后端 user.go:175-181 对邀请码的校验和 RecordUse 都不区分
|
||||
// 注册路径(`if inviteCode != ""` 就会校验并记录),而且 user.go:169 的
|
||||
// inviteRequired 在短信通道关闭时对手机号注册同样为真。UI 上明明摆着邀请码输入框,
|
||||
// 原来却硬传空串,用户填了也被静默丢弃,邀请来源统计全丢。
|
||||
{username: form.username || '', phone: form.phone, invite_code: form.invite || ''},
|
||||
{meth: 'tel', val: form.phone, resp: verified.verification_token, params: {challenge_id: challengeId}});
|
||||
} else {
|
||||
onImRegisterRequest(form.username, form.password, form.username,
|
||||
@@ -546,6 +550,20 @@ function IdentityCheck({authenticated}) {
|
||||
setSeconds(data.resend_after_seconds || 60);
|
||||
} catch (err) {
|
||||
setError(err.reason === 'REALNAME_REQUIRED' ? '需要先完成实名认证并绑定手机号' : (err.message || '获取验证码失败'));
|
||||
// 限流(429)时服务端会连同还没过期的 challenge_id 和剩余冷却秒数一起返回
|
||||
// (im_http.go:6361-6367)。原来这里只把错误文案显示出来就完了,challengeId 一直是空串,
|
||||
// 于是 submit() 开头的 `if (!challengeId) return` 会静默吞掉点击——按钮看着能点,
|
||||
// 输完 6 位验证码点"确定"毫无反应,也没有任何提示。同文件的 VerificationButton
|
||||
// 早就处理对了,这里是漏抄。
|
||||
if (err.details && err.details.retry_after_seconds) {
|
||||
setSeconds(err.details.retry_after_seconds);
|
||||
}
|
||||
if (err.details && err.details.challenge_id) {
|
||||
setChallengeId(err.details.challenge_id);
|
||||
}
|
||||
if (err.details && err.details.phone_mask) {
|
||||
setPhoneMask(err.details.phone_mask);
|
||||
}
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
@@ -148,7 +148,11 @@ function AddressBook({myUserId, onTopicSelected}) {
|
||||
{loading ? <p className="people-empty-hint">加载中…</p> : sortedKeys.length === 0 ? <p className="people-empty-hint">暂无好友</p> : null}
|
||||
{sortedKeys.map(initial => <section className="people-group" id={'people-group-' + initial} key={initial}><h2>{initial}</h2>
|
||||
{groups[initial].map(person => <button type="button" key={person.user_id} onClick={() => { seedProfile(person); navigate('friend-profile', person.user_id); }}>
|
||||
<Avatar person={person} /><span><strong>{person.remark || person.display_name}</strong>{person.remark ? <small>{person.display_name}</small> : null}</span>
|
||||
{/* 副行只在确实是另一个名字时才显示。后端在有备注时会把 display_name 也设成备注
|
||||
(im_http.go:8500-8507,且合并守卫会挡住用 peer 真实昵称覆盖),两个字段同值,
|
||||
原来那句 `person.remark ? <small>{display_name}</small>` 会把同一串字渲染两遍。
|
||||
后端目前没有单独的原昵称字段,所以拿不到"备注(主)/昵称(副)"的第二行,已记为后端缺口。 */}
|
||||
<Avatar person={person} /><span><strong>{person.remark || person.display_name}</strong>{person.remark && person.remark !== person.display_name ? <small>{person.display_name}</small> : null}</span>
|
||||
</button>)}
|
||||
</section>)}
|
||||
</main>
|
||||
@@ -377,7 +381,7 @@ function Profile({uid, friend, onTopicSelected, onToast}) {
|
||||
|
||||
function Toggle({checked, onChange}) { return <button type="button" role="switch" aria-checked={checked} className={'people-switch' + (checked ? ' checked' : '')} onClick={() => onChange(!checked)}><i /></button>; }
|
||||
|
||||
function Settings({uid, initialBlocked, onToast}) {
|
||||
function Settings({uid, initialBlocked, pinBlocked, onToast}) {
|
||||
// Same seed-from-what-we-already-know as Profile/Verification -- getUserProfile can 403 even for a
|
||||
// real, current friend (see seedProfile's own comment), which used to leave this page's header
|
||||
// blank instead of just stuck like Profile, but no less wrong.
|
||||
@@ -390,7 +394,11 @@ function Settings({uid, initialBlocked, onToast}) {
|
||||
const [remarkDraft, setRemarkDraft] = React.useState('');
|
||||
React.useEffect(() => {
|
||||
if (!uid) return;
|
||||
getUserProfile(uid).then(p => { writeCachedData('profile-seed:' + uid, p); setProfile(p); setBlocked(p.relationship === 'blocked_by_me'); }).catch(() => {});
|
||||
// pinBlocked:确认弹窗把这个页面当背景渲染,并用 initialBlocked 摆出"操作之后"的样子。
|
||||
// 这时不能让这次拉取回来的真实关系(还是 friend)把它改回去——原来会导致弹窗一弹出来,
|
||||
// 背后的拉黑开关自己跳回"未拉黑",同时"删除好友"红条又冒出来,跟参考图不符也跟弹窗
|
||||
// 正在确认的这件事自相矛盾。
|
||||
getUserProfile(uid).then(p => { writeCachedData('profile-seed:' + uid, p); setProfile(p); if (!pinBlocked) setBlocked(p.relationship === 'blocked_by_me'); }).catch(() => {});
|
||||
// GET /users/{id}/profile never exposes the real remark (backend's userProfilePayload always
|
||||
// calls userSummaryPayload(user, "") -- confirmed against im_be/server/im_http.go:7844 --
|
||||
// folding an empty remark into display_name instead of returning a separate field), only the
|
||||
@@ -480,7 +488,7 @@ function ConfirmSheet({type, uid, tinode, onToast}) {
|
||||
call.then(() => { onToast(config.toast); navigate(config.route, config.keepUid ? uid : undefined); })
|
||||
.catch(err => onToast(err.message || '操作失败')).finally(() => setBusy(false));
|
||||
};
|
||||
return <div className="people-page"><Settings uid={uid} initialBlocked={type === 'block'} onToast={onToast} />
|
||||
return <div className="people-page"><Settings uid={uid} initialBlocked={type === 'block'} pinBlocked={true} onToast={onToast} />
|
||||
<div className="people-sheet-backdrop"><section className="people-confirm-sheet" role="dialog" aria-modal="true"><i /><h2>{config.title}</h2><p>{config.text}</p>
|
||||
<button type="button" className="danger" disabled={busy} onClick={confirm}>{config.action}</button>
|
||||
<button type="button" onClick={() => navigate('friend-settings', uid)}>取消</button>
|
||||
|
||||
@@ -24,8 +24,24 @@ const navigate = (view, topic) => {
|
||||
// 'group-member-profile' is contact-management-view.jsx's Profile component (Ch.5) reused as-is for
|
||||
// viewing a group member who isn't necessarily a friend -- that route keys off `uid`, not `topic`, so it
|
||||
// can't go through this file's own `navigate` helper above without clobbering the wrong hash param.
|
||||
const navigateToProfile = userId => HashNavigation.navigateTo(
|
||||
// 除了跳转,还必须先把已知资料写进 'profile-seed:' 缓存 —— 目标页(第5章的 Profile)会先读这个种子,
|
||||
// 因为 GET /users/{id}/profile 对非好友按隐私默认直接 403(imPrivacyAllowsProfile)。群成员里
|
||||
// 大部分不是好友,不 seed 的话点进去就是"资料加载失败,请重试"。第5章自己的入口
|
||||
// (contact-management-view.jsx 的 seedProfile)一直是这么做的,这里漏了。
|
||||
const navigateToProfile = (userId, member) => {
|
||||
if (member && userId) {
|
||||
writeCachedData('profile-seed:' + userId, {
|
||||
user_id: userId,
|
||||
display_name: member.group_nickname || member.display_name || userId,
|
||||
avatar_file_id: member.avatar_file_id,
|
||||
...(member.signature ? {signature: member.signature} : {}),
|
||||
...(member.region ? {region: member.region} : {}),
|
||||
...(member.gender ? {gender: member.gender} : {})
|
||||
});
|
||||
}
|
||||
HashNavigation.navigateTo(
|
||||
HashNavigation.addUrlParam(HashNavigation.setUrlSidePanel(window.location.hash, 'group-member-profile'), 'uid', userId));
|
||||
};
|
||||
|
||||
export const GROUP_MANAGEMENT_VIEWS = [
|
||||
'group-list', 'group-empty', 'group-invite', 'group-events', 'group-join-requests',
|
||||
@@ -60,6 +76,27 @@ function groupBy(list, keyFn) {
|
||||
return Object.keys(groups).sort().map(key => [key, groups[key]]);
|
||||
}
|
||||
|
||||
// GET /v1/im/groups 的 name/member_count 目前不可信:handler 用的是 store.Users.GetSubs,而 store 自己的
|
||||
// 注释就写着它 "Does not load Public/Trusted or Private"(store.go:834-835),可 groupListItemPayload
|
||||
// 偏偏去读 sub.GetPublic() 取名字、sub.GetSubCnt() 取人数——于是每个群都返回自己的 topic id 当名字、
|
||||
// 0 当人数(应该用 GetTopics,它会把 Public 拷到 subscription 上)。已记入后端问题清单。
|
||||
// 在后端修好之前,用本地 Tinode 会话缓存兜底——消息首页的群名一直是从这里取的,是准的。
|
||||
function groupDisplayName(tinode, group) {
|
||||
if (group.name && group.name !== group.topic) return group.name;
|
||||
const t = tinode && tinode.getTopic ? tinode.getTopic(group.topic) : null;
|
||||
const pub = t && t.public;
|
||||
// 群的 public 用的是 `name`(实测本地缓存就是 {name:"扫码测试群", version:1}),后端自己的
|
||||
// groupName() 也是按 name/display_name/fn 这个顺序找的,保持一致。
|
||||
const local = pub && (pub.name || pub.display_name || pub.fn);
|
||||
return local || group.name || group.topic;
|
||||
}
|
||||
|
||||
function groupMemberCount(tinode, group) {
|
||||
if (group.member_count) return group.member_count;
|
||||
const t = tinode && tinode.getTopic ? tinode.getTopic(group.topic) : null;
|
||||
return (t && t.subcnt) || 0;
|
||||
}
|
||||
|
||||
function shortDate(iso) {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
@@ -82,9 +119,11 @@ const GROUPS_DEMO = {
|
||||
};
|
||||
|
||||
function Icon({children}) { return <i className="material-icons" aria-hidden="true">{children}</i>; }
|
||||
function Header({title, back = 'group-list', backTopic, right, onRight}) {
|
||||
// onBack:少数页面的返回目标不是"另一个侧栏视图",而是要关掉侧栏回到某个会话(需要写 path[1],
|
||||
// navigate 辅助函数只会写 query 参数),这类情况传一个自己的回调进来。
|
||||
function Header({title, back = 'group-list', backTopic, onBack, right, onRight}) {
|
||||
return <header className="group-header">
|
||||
<button type="button" onClick={() => { if (!HashNavigation.goBack()) navigate(back, backTopic); }} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button>
|
||||
<button type="button" onClick={() => { if (!HashNavigation.goBack()) { if (onBack) onBack(); else navigate(back, backTopic); } }} aria-label="返回"><Icon>arrow_back_ios_new</Icon></button>
|
||||
<h1>{title}</h1>
|
||||
{right ? <button type="button" className="text" onClick={onRight}>{right}</button> : <span />}
|
||||
</header>;
|
||||
@@ -118,7 +157,7 @@ function Toggle({checked, onChange, disabled}) {
|
||||
className={'group-switch' + (checked ? ' checked' : '')} onClick={() => !disabled && onChange(!checked)}><i /></button>;
|
||||
}
|
||||
|
||||
function GroupList({onToast}) {
|
||||
function GroupList({tinode, onToast}) {
|
||||
// Only the unfiltered (query === '') result is worth caching -- caching a search result under the
|
||||
// same key would show stale search results instead of the real list on the next plain visit.
|
||||
const [lists, setLists] = React.useState(() => readCachedData('groups') || {owned: null, managed: null, joined: null});
|
||||
@@ -163,7 +202,7 @@ function GroupList({onToast}) {
|
||||
current.length ? current.map(group =>
|
||||
<button type="button" key={group.topic} onClick={() => navigate('group-settings', group.topic)}>
|
||||
<Avatar item={group} />
|
||||
<span><strong>{group.name}</strong><small>{group.member_count} 位成员</small></span>
|
||||
<span><strong>{groupDisplayName(tinode, group)}</strong><small>{groupMemberCount(tinode, group)} 位成员</small></span>
|
||||
<Icon>chevron_right</Icon>
|
||||
</button>) :
|
||||
<div className="group-empty"><Icon>groups</Icon><strong>这里还没有群聊</strong>
|
||||
@@ -268,7 +307,13 @@ function Announcement({topic, myRole, fromChat, onToast}) {
|
||||
// handleShowAnnouncement) vs. from group-settings' "群公告" row land on the identical
|
||||
// path[0]=group-announcement-detail hash -- `fromChat` is the only thing telling them apart, so
|
||||
// "back" can return to the chat (close the sidepanel) instead of always forcing group-settings.
|
||||
const backView = fromChat ? '' : 'group-settings';
|
||||
// 从聊天页横幅进来时(messages-view.jsx:1152 用空基准拼 hash,所以 path[1] 是空的),返回应该
|
||||
// 关掉侧栏回到那个会话。原来写的是 navigate('', topic)——navigate 只把 topic 写成 query 参数,
|
||||
// path[1] 依旧为空,tinode-web 的 handleHashRoute 就把 topicSelected 清成 null,会话跟着一起关了。
|
||||
// 这里显式把 topic 写回 path[1]。
|
||||
const backToChat = React.useCallback(() => {
|
||||
HashNavigation.navigateTo(HashNavigation.setUrlTopic(HashNavigation.setUrlSidePanel('', ''), topic));
|
||||
}, [topic]);
|
||||
React.useEffect(() => {
|
||||
if (!topic) return;
|
||||
const cached = readCachedData('group-announcement:' + topic);
|
||||
@@ -286,9 +331,9 @@ function Announcement({topic, myRole, fromChat, onToast}) {
|
||||
setData(d); setEditing(false); onToast('群公告已发布');
|
||||
}).catch(err => onToast(err.message || '发布失败')).finally(() => setSaving(false));
|
||||
};
|
||||
if (!data) return <div className="group-page"><Header title="群公告" back={backView} backTopic={topic} /><main className="group-announcement-detail"><p className="group-inline-empty">{loadError ? '群公告加载失败' : '加载中…'}</p></main></div>;
|
||||
if (!data) return <div className="group-page"><Header title="群公告" back={fromChat ? undefined : 'group-settings'} backTopic={topic} onBack={fromChat ? backToChat : undefined} /><main className="group-announcement-detail"><p className="group-inline-empty">{loadError ? '群公告加载失败' : '加载中…'}</p></main></div>;
|
||||
return <div className="group-page">
|
||||
<Header title="群公告" back={backView} backTopic={topic}
|
||||
<Header title="群公告" back={fromChat ? undefined : 'group-settings'} backTopic={topic} onBack={fromChat ? backToChat : undefined}
|
||||
right={canEdit ? '发布' : null} onRight={canEdit ? publish : undefined} />
|
||||
<main className="group-announcement-detail">
|
||||
{canEdit ? <textarea autoFocus={editing} value={draft} maxLength={2000}
|
||||
@@ -377,7 +422,7 @@ function GroupSettings({topic, myUserId, tinode, onToast}) {
|
||||
<strong>群成员</strong><small>共 {group.member_count} 位成员</small>
|
||||
</div>
|
||||
<div className="group-member-grid">
|
||||
{members.slice(0, 9).map(item => <button type="button" key={item.user_id} onClick={() => navigateToProfile(item.user_id)}>
|
||||
{members.slice(0, 9).map(item => <button type="button" key={item.user_id} onClick={() => navigateToProfile(item.user_id, item)}>
|
||||
<Avatar item={item} icon="person" /><small>{item.group_nickname || item.display_name}</small>
|
||||
</button>)}
|
||||
<button type="button" onClick={() => onToast('请通过转发/分享群二维码邀请成员')}>
|
||||
@@ -712,6 +757,7 @@ function Muted({topic, onToast}) {
|
||||
// everyone still in the group, and fetching it here is one extra request for the whole list
|
||||
// (not per-row), unlike JoinRequests' per-item getUserProfile lookup.
|
||||
const [names, setNames] = React.useState({});
|
||||
const [avatars, setAvatars] = React.useState({});
|
||||
const [loadError, setLoadError] = React.useState(false);
|
||||
const load = React.useCallback(() => {
|
||||
if (!topic) return;
|
||||
@@ -722,8 +768,13 @@ function Muted({topic, onToast}) {
|
||||
}).catch(() => { onToast('加载失败'); setLoadError(true); });
|
||||
getGroupMembers(topic).then(data => {
|
||||
const map = {};
|
||||
(data.items || []).forEach(m => { map[m.user_id] = m.group_nickname || m.display_name || m.user_id; });
|
||||
const avatarMap = {};
|
||||
(data.items || []).forEach(m => {
|
||||
map[m.user_id] = m.group_nickname || m.display_name || m.user_id;
|
||||
if (m.avatar_file_id) avatarMap[m.user_id] = {avatar_file_id: m.avatar_file_id};
|
||||
});
|
||||
setNames(map);
|
||||
setAvatars(avatarMap);
|
||||
}).catch(() => {});
|
||||
}, [topic]);
|
||||
React.useEffect(() => { setItems((topic && readCachedData('group-muted:' + topic)) || null); load(); }, [topic, load]);
|
||||
@@ -736,9 +787,15 @@ function Muted({topic, onToast}) {
|
||||
<main className="group-muted-list">
|
||||
{items === null ? <p className="group-inline-empty">{loadError ? '成员列表加载失败' : '加载中…'}</p> :
|
||||
items.length ? items.map(item => <article key={item.user_id}>
|
||||
<Avatar item={{}} /><span><strong>{names[item.user_id] || item.user_id}</strong><small>{item.expires_at ? `禁言至 ${shortDate(item.expires_at)}` : '永久禁言'}</small></span>
|
||||
{/* 必须传 icon="person":Avatar 的兜底图标默认是 'group',禁言的是人不是群,
|
||||
原来每一行都画成群组图标。getGroupMutes 本身不返回 avatar_file_id
|
||||
(im_http.go:7083-7096),但同组件已经为了解析显示名拉过整份成员表,顺带把头像也带上。 */}
|
||||
<Avatar item={avatars[item.user_id] || {}} icon="person" /><span><strong>{names[item.user_id] || item.user_id}</strong><small>{item.expires_at ? `禁言至 ${shortDate(item.expires_at)}` : '永久禁言'}</small></span>
|
||||
<button type="button" onClick={() => unmute(item.user_id)}>解除</button>
|
||||
</article>) : <div className="group-inline-empty">以上成员不允许发言</div>}
|
||||
</article>) : <div className="group-inline-empty">暂无禁言成员</div>}
|
||||
{/* "以上成员不允许发言"在参考图里是列表下方的常驻灰色注脚,不是空状态文案。
|
||||
原来把它当空态用,结果有禁言成员时这句话消失、一个都没有时反而显示它,语义正好反了。 */}
|
||||
{items && items.length ? <p className="group-inline-note">以上成员不允许发言</p> : null}
|
||||
</main>
|
||||
</div>;
|
||||
}
|
||||
@@ -797,6 +854,9 @@ function GroupFiles({topic, tinode, onToast}) {
|
||||
const [menuOpen, setMenuOpen] = React.useState(false);
|
||||
const [uploading, setUploading] = React.useState(null);
|
||||
const [loadError, setLoadError] = React.useState(false);
|
||||
// groupFilePayload 只给 uploader_id、不给昵称(im_http.go:6532-6559),原来直接把裸 user_id
|
||||
// 显示在"来自"位置。跟 Muted 页一样,拉一次成员表交叉引用出可读的名字。
|
||||
const [uploaderNames, setUploaderNames] = React.useState({});
|
||||
const inputRefs = {file: React.useRef(null), image: React.useRef(null), video: React.useRef(null)};
|
||||
const load = React.useCallback(() => {
|
||||
if (!topic) return;
|
||||
@@ -807,6 +867,14 @@ function GroupFiles({topic, tinode, onToast}) {
|
||||
}).catch(() => { onToast('加载失败'); setLoadError(true); });
|
||||
}, [topic, query]);
|
||||
React.useEffect(() => { load(); }, [load]);
|
||||
React.useEffect(() => {
|
||||
if (!topic) return;
|
||||
getGroupMembers(topic).then(data => {
|
||||
const map = {};
|
||||
(data.items || []).forEach(m => { map[m.user_id] = m.group_nickname || m.display_name || m.user_id; });
|
||||
setUploaderNames(map);
|
||||
}).catch(() => {});
|
||||
}, [topic]);
|
||||
const usedBytes = (files || []).reduce((sum, f) => sum + (f.file_size || 0), 0);
|
||||
const pick = kind => { setMenuOpen(false); inputRefs[kind].current && inputRefs[kind].current.click(); };
|
||||
const onFile = e => {
|
||||
@@ -846,7 +914,7 @@ function GroupFiles({topic, tinode, onToast}) {
|
||||
const [icon, color] = FILE_TYPE_META[file.file_type] || FILE_TYPE_META.file;
|
||||
return <article key={file.group_file_id}>
|
||||
<span className="group-file-type" style={{backgroundColor: color}}><Icon>{icon}</Icon></span>
|
||||
<div><strong>{file.file_name || '未命名文件'}</strong><small>{bytesToGB(file.file_size)} · {file.uploader_id}</small></div>
|
||||
<div><strong>{file.file_name || '未命名文件'}</strong><small>来自 {uploaderNames[file.uploader_id] || file.uploader_id} · {bytesToGB(file.file_size)}</small></div>
|
||||
<button type="button" onClick={() => remove(file.group_file_id)}><Icon>delete_outline</Icon></button>
|
||||
</article>;
|
||||
})}
|
||||
@@ -854,10 +922,12 @@ function GroupFiles({topic, tinode, onToast}) {
|
||||
</div>;
|
||||
}
|
||||
|
||||
// 名字叫 bytesToGB 却只输出到 MB —— 1.4GB 的文件会显示成 "1433.6 MB"。补上 GB 档。
|
||||
function bytesToGB(bytes) {
|
||||
if (!bytes) return '0 KB';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||||
}
|
||||
|
||||
function GroupNameEditor({topic, onToast}) {
|
||||
@@ -941,7 +1011,7 @@ export default function GroupManagementView({view, hashParams, myUserId, tinode,
|
||||
}).catch(() => {});
|
||||
}, [topic, myUserId]);
|
||||
let content;
|
||||
if (view === 'group-list' || view === 'group-empty') content = <GroupList onToast={setToast} />;
|
||||
if (view === 'group-list' || view === 'group-empty') content = <GroupList tinode={tinode} onToast={setToast} />;
|
||||
else if (['group-invite', 'group-events', 'group-new-request', 'group-announcement', 'group-summary', 'group-role-message'].includes(view)) content = <GroupChat mode={view} onToast={setToast} />;
|
||||
else if (view === 'group-join-requests') content = <JoinRequests topic={topic} onToast={setToast} />;
|
||||
else if (view === 'group-announcement-detail') content = <Announcement topic={topic} myRole={myRole} fromChat={hashParams && hashParams.from === 'chat'} onToast={setToast} />;
|
||||
|
||||
@@ -957,10 +957,15 @@ class TinodeWeb extends React.Component {
|
||||
console.warn("Login failed", err);
|
||||
if (!autoLogin) {
|
||||
if (token) {
|
||||
// 存的 token 失效(过期/被撤销)——这属于"会话没了",回到登录入口是对的。
|
||||
this.handleLogout();
|
||||
}
|
||||
HashNavigation.navigateTo('');
|
||||
}
|
||||
// 用户手输账号密码失败(401 之类)时**不要**导航。原来这里无条件 navigateTo(''),
|
||||
// 会把 sidePanelSelected 清成 '',render 里 '' 又落到 'tenant',于是密码打错一次就被
|
||||
// 弹回企业码页,刚输的账号丢了;而 TenantEntry 不渲染 FormAlert、chapterOne 分支也不渲染
|
||||
// ErrorPanel,那句错误提示用户根本看不到。留在登录页,errorText 才能显示出来。
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+91
-19
@@ -6155,7 +6155,7 @@ function Register({
|
||||
onImRegisterRequest(secretAccount, form.password, form.username || '', {
|
||||
username: form.username || '',
|
||||
phone: form.phone,
|
||||
invite_code: ''
|
||||
invite_code: form.invite || ''
|
||||
}, {
|
||||
meth: 'tel',
|
||||
val: form.phone,
|
||||
@@ -6393,6 +6393,15 @@ function IdentityCheck({
|
||||
setSeconds(data.resend_after_seconds || 60);
|
||||
} catch (err) {
|
||||
setError(err.reason === 'REALNAME_REQUIRED' ? '需要先完成实名认证并绑定手机号' : err.message || '获取验证码失败');
|
||||
if (err.details && err.details.retry_after_seconds) {
|
||||
setSeconds(err.details.retry_after_seconds);
|
||||
}
|
||||
if (err.details && err.details.challenge_id) {
|
||||
setChallengeId(err.details.challenge_id);
|
||||
}
|
||||
if (err.details && err.details.phone_mask) {
|
||||
setPhoneMask(err.details.phone_mask);
|
||||
}
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
@@ -7614,7 +7623,7 @@ function AddressBook({
|
||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("span", {
|
||||
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("strong", {
|
||||
children: person.remark || person.display_name
|
||||
}, void 0, false), person.remark ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("small", {
|
||||
}, void 0, false), person.remark && person.remark !== person.display_name ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("small", {
|
||||
children: person.display_name
|
||||
}, void 0, false) : null]
|
||||
}, void 0, true)]
|
||||
@@ -8111,6 +8120,7 @@ function Toggle({
|
||||
function Settings({
|
||||
uid,
|
||||
initialBlocked,
|
||||
pinBlocked,
|
||||
onToast
|
||||
}) {
|
||||
const [profile, setProfile] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(() => uid ? (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.readCachedData)('profile-seed:' + uid) : null);
|
||||
@@ -8125,7 +8135,7 @@ function Settings({
|
||||
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getUserProfile)(uid).then(p => {
|
||||
(0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.writeCachedData)('profile-seed:' + uid, p);
|
||||
setProfile(p);
|
||||
setBlocked(p.relationship === 'blocked_by_me');
|
||||
if (!pinBlocked) setBlocked(p.relationship === 'blocked_by_me');
|
||||
}).catch(() => {});
|
||||
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getFriends)().then(data => {
|
||||
const entry = (data.items || []).find(item => item.user_id === uid);
|
||||
@@ -8358,6 +8368,7 @@ function ConfirmSheet({
|
||||
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Settings, {
|
||||
uid: uid,
|
||||
initialBlocked: type === 'block',
|
||||
pinBlocked: true,
|
||||
onToast: onToast
|
||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("div", {
|
||||
className: "people-sheet-backdrop",
|
||||
@@ -10319,7 +10330,25 @@ const navigate = (view, topic) => {
|
||||
hash = topic ? _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(hash, 'topic', topic) : _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].removeUrlParam(hash, 'topic');
|
||||
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].navigateTo(hash);
|
||||
};
|
||||
const navigateToProfile = userId => _lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel(window.location.hash, 'group-member-profile'), 'uid', userId));
|
||||
const navigateToProfile = (userId, member) => {
|
||||
if (member && userId) {
|
||||
(0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.writeCachedData)('profile-seed:' + userId, {
|
||||
user_id: userId,
|
||||
display_name: member.group_nickname || member.display_name || userId,
|
||||
avatar_file_id: member.avatar_file_id,
|
||||
...(member.signature ? {
|
||||
signature: member.signature
|
||||
} : {}),
|
||||
...(member.region ? {
|
||||
region: member.region
|
||||
} : {}),
|
||||
...(member.gender ? {
|
||||
gender: member.gender
|
||||
} : {})
|
||||
});
|
||||
}
|
||||
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].addUrlParam(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel(window.location.hash, 'group-member-profile'), 'uid', userId));
|
||||
};
|
||||
const GROUP_MANAGEMENT_VIEWS = ['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'];
|
||||
function alphaInitial(name) {
|
||||
const ch = (name || '').trim()[0] || '#';
|
||||
@@ -10343,6 +10372,18 @@ function groupBy(list, keyFn) {
|
||||
});
|
||||
return Object.keys(groups).sort().map(key => [key, groups[key]]);
|
||||
}
|
||||
function groupDisplayName(tinode, group) {
|
||||
if (group.name && group.name !== group.topic) return group.name;
|
||||
const t = tinode && tinode.getTopic ? tinode.getTopic(group.topic) : null;
|
||||
const pub = t && t.public;
|
||||
const local = pub && (pub.name || pub.display_name || pub.fn);
|
||||
return local || group.name || group.topic;
|
||||
}
|
||||
function groupMemberCount(tinode, group) {
|
||||
if (group.member_count) return group.member_count;
|
||||
const t = tinode && tinode.getTopic ? tinode.getTopic(group.topic) : null;
|
||||
return t && t.subcnt || 0;
|
||||
}
|
||||
function shortDate(iso) {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
@@ -10395,6 +10436,7 @@ function Header({
|
||||
title,
|
||||
back = 'group-list',
|
||||
backTopic,
|
||||
onBack,
|
||||
right,
|
||||
onRight
|
||||
}) {
|
||||
@@ -10403,7 +10445,9 @@ function Header({
|
||||
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("button", {
|
||||
type: "button",
|
||||
onClick: () => {
|
||||
if (!_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].goBack()) navigate(back, backTopic);
|
||||
if (!_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].goBack()) {
|
||||
if (onBack) onBack();else navigate(back, backTopic);
|
||||
}
|
||||
},
|
||||
"aria-label": "返回",
|
||||
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Icon, {
|
||||
@@ -10514,6 +10558,7 @@ function Toggle({
|
||||
}, void 0, false);
|
||||
}
|
||||
function GroupList({
|
||||
tinode,
|
||||
onToast
|
||||
}) {
|
||||
const [lists, setLists] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(() => (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.readCachedData)('groups') || {
|
||||
@@ -10602,9 +10647,9 @@ function GroupList({
|
||||
item: group
|
||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("span", {
|
||||
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("strong", {
|
||||
children: group.name
|
||||
children: groupDisplayName(tinode, group)
|
||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("small", {
|
||||
children: [group.member_count, " 位成员"]
|
||||
children: [groupMemberCount(tinode, group), " 位成员"]
|
||||
}, void 0, true)]
|
||||
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Icon, {
|
||||
children: "chevron_right"
|
||||
@@ -10919,7 +10964,9 @@ function Announcement({
|
||||
const [saving, setSaving] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
|
||||
const [loadError, setLoadError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
|
||||
const canEdit = myRole === 'owner' || myRole === 'admin';
|
||||
const backView = fromChat ? '' : 'group-settings';
|
||||
const backToChat = react__WEBPACK_IMPORTED_MODULE_0___default().useCallback(() => {
|
||||
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].navigateTo(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlTopic(_lib_navigation_js__WEBPACK_IMPORTED_MODULE_3__["default"].setUrlSidePanel('', ''), topic));
|
||||
}, [topic]);
|
||||
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
|
||||
if (!topic) return;
|
||||
const cached = (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.readCachedData)('group-announcement:' + topic);
|
||||
@@ -10950,8 +10997,9 @@ function Announcement({
|
||||
className: "group-page",
|
||||
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Header, {
|
||||
title: "群公告",
|
||||
back: backView,
|
||||
backTopic: topic
|
||||
back: fromChat ? undefined : 'group-settings',
|
||||
backTopic: topic,
|
||||
onBack: fromChat ? backToChat : undefined
|
||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("main", {
|
||||
className: "group-announcement-detail",
|
||||
children: (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("p", {
|
||||
@@ -10964,8 +11012,9 @@ function Announcement({
|
||||
className: "group-page",
|
||||
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Header, {
|
||||
title: "群公告",
|
||||
back: backView,
|
||||
back: fromChat ? undefined : 'group-settings',
|
||||
backTopic: topic,
|
||||
onBack: fromChat ? backToChat : undefined,
|
||||
right: canEdit ? '发布' : null,
|
||||
onRight: canEdit ? publish : undefined
|
||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("main", {
|
||||
@@ -11106,7 +11155,7 @@ function GroupSettings({
|
||||
className: "group-member-grid",
|
||||
children: [members.slice(0, 9).map(item => (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("button", {
|
||||
type: "button",
|
||||
onClick: () => navigateToProfile(item.user_id),
|
||||
onClick: () => navigateToProfile(item.user_id, item),
|
||||
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Avatar, {
|
||||
item: item,
|
||||
icon: "person"
|
||||
@@ -11793,6 +11842,7 @@ function Muted({
|
||||
}) {
|
||||
const [items, setItems] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(() => topic && (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.readCachedData)('group-muted:' + topic) || null);
|
||||
const [names, setNames] = react__WEBPACK_IMPORTED_MODULE_0___default().useState({});
|
||||
const [avatars, setAvatars] = react__WEBPACK_IMPORTED_MODULE_0___default().useState({});
|
||||
const [loadError, setLoadError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
|
||||
const load = react__WEBPACK_IMPORTED_MODULE_0___default().useCallback(() => {
|
||||
if (!topic) return;
|
||||
@@ -11806,10 +11856,15 @@ function Muted({
|
||||
});
|
||||
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroupMembers)(topic).then(data => {
|
||||
const map = {};
|
||||
const avatarMap = {};
|
||||
(data.items || []).forEach(m => {
|
||||
map[m.user_id] = m.group_nickname || m.display_name || m.user_id;
|
||||
if (m.avatar_file_id) avatarMap[m.user_id] = {
|
||||
avatar_file_id: m.avatar_file_id
|
||||
};
|
||||
});
|
||||
setNames(map);
|
||||
setAvatars(avatarMap);
|
||||
}).catch(() => {});
|
||||
}, [topic]);
|
||||
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
|
||||
@@ -11832,12 +11887,13 @@ function Muted({
|
||||
onRight: () => navigate('group-mute-add', topic)
|
||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("main", {
|
||||
className: "group-muted-list",
|
||||
children: items === null ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("p", {
|
||||
children: [items === null ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("p", {
|
||||
className: "group-inline-empty",
|
||||
children: loadError ? '成员列表加载失败' : '加载中…'
|
||||
}, void 0, false) : items.length ? items.map(item => (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("article", {
|
||||
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Avatar, {
|
||||
item: {}
|
||||
item: avatars[item.user_id] || {},
|
||||
icon: "person"
|
||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("span", {
|
||||
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("strong", {
|
||||
children: names[item.user_id] || item.user_id
|
||||
@@ -11851,9 +11907,12 @@ function Muted({
|
||||
}, void 0, false)]
|
||||
}, item.user_id, true)) : (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("div", {
|
||||
className: "group-inline-empty",
|
||||
children: "暂无禁言成员"
|
||||
}, void 0, false), items && items.length ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("p", {
|
||||
className: "group-inline-note",
|
||||
children: "以上成员不允许发言"
|
||||
}, void 0, false)
|
||||
}, void 0, false)]
|
||||
}, void 0, false) : null]
|
||||
}, void 0, true)]
|
||||
}, void 0, true);
|
||||
}
|
||||
function OwnerSelect({
|
||||
@@ -11965,6 +12024,7 @@ function GroupFiles({
|
||||
const [menuOpen, setMenuOpen] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
|
||||
const [uploading, setUploading] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(null);
|
||||
const [loadError, setLoadError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
|
||||
const [uploaderNames, setUploaderNames] = react__WEBPACK_IMPORTED_MODULE_0___default().useState({});
|
||||
const inputRefs = {
|
||||
file: react__WEBPACK_IMPORTED_MODULE_0___default().useRef(null),
|
||||
image: react__WEBPACK_IMPORTED_MODULE_0___default().useRef(null),
|
||||
@@ -11986,6 +12046,16 @@ function GroupFiles({
|
||||
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
|
||||
if (!topic) return;
|
||||
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getGroupMembers)(topic).then(data => {
|
||||
const map = {};
|
||||
(data.items || []).forEach(m => {
|
||||
map[m.user_id] = m.group_nickname || m.display_name || m.user_id;
|
||||
});
|
||||
setUploaderNames(map);
|
||||
}).catch(() => {});
|
||||
}, [topic]);
|
||||
const usedBytes = (files || []).reduce((sum, f) => sum + (f.file_size || 0), 0);
|
||||
const pick = kind => {
|
||||
setMenuOpen(false);
|
||||
@@ -12122,7 +12192,7 @@ function GroupFiles({
|
||||
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("strong", {
|
||||
children: file.file_name || '未命名文件'
|
||||
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("small", {
|
||||
children: [bytesToGB(file.file_size), " · ", file.uploader_id]
|
||||
children: ["来自 ", uploaderNames[file.uploader_id] || file.uploader_id, " · ", bytesToGB(file.file_size)]
|
||||
}, void 0, true)]
|
||||
}, void 0, true), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("button", {
|
||||
type: "button",
|
||||
@@ -12139,7 +12209,8 @@ function GroupFiles({
|
||||
function bytesToGB(bytes) {
|
||||
if (!bytes) return '0 KB';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||||
}
|
||||
function GroupNameEditor({
|
||||
topic,
|
||||
@@ -12292,6 +12363,7 @@ function GroupManagementView({
|
||||
}, [topic, myUserId]);
|
||||
let content;
|
||||
if (view === 'group-list' || view === 'group-empty') content = (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(GroupList, {
|
||||
tinode: tinode,
|
||||
onToast: setToast
|
||||
}, void 0, false);else if (['group-invite', 'group-events', 'group-new-request', 'group-announcement', 'group-summary', 'group-role-message'].includes(view)) content = (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(GroupChat, {
|
||||
mode: view,
|
||||
@@ -17774,9 +17846,9 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
|
||||
if (!autoLogin) {
|
||||
if (token) {
|
||||
this.handleLogout();
|
||||
}
|
||||
_lib_navigation_js__WEBPACK_IMPORTED_MODULE_20__["default"].navigateTo('');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
static navigateToCredentialsView(params) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user