通讯录/新朋友/黑名单列表区分"加载失败"与"真的是空"

三处都用 .catch(() => {}) 吞掉网络错误,loading 结束后空数组直接落到
"暂无好友"/"暂无新朋友申请"/"黑名单为空",加载失败和真的没数据在界面
上无法区分。改成 group-management-view.jsx 里 11 处同类组件已经在用
的 loadError 状态模式。

第十轮最小粒度测试 Ch5-6 发现。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-06 00:57:17 +08:00
co-authored by Claude Sonnet 5
parent 4dbe76f2bd
commit 89fdd31cd4
3 changed files with 19 additions and 13 deletions
+9 -6
View File
@@ -106,6 +106,7 @@ function AddressBook({myUserId, onTopicSelected}) {
const [friends, setFriends] = React.useState(() => (friendsCacheKey && readCachedData(friendsCacheKey)) || []); const [friends, setFriends] = React.useState(() => (friendsCacheKey && readCachedData(friendsCacheKey)) || []);
const [pendingCount, setPendingCount] = React.useState(0); const [pendingCount, setPendingCount] = React.useState(0);
const [loading, setLoading] = React.useState(() => !(friendsCacheKey && readCachedData(friendsCacheKey))); const [loading, setLoading] = React.useState(() => !(friendsCacheKey && readCachedData(friendsCacheKey)));
const [loadError, setLoadError] = React.useState(false);
// Same bootstrap the message center already fetches (and caches under the same key) -- only need // Same bootstrap the message center already fetches (and caches under the same key) -- only need
// it here for the file assistant's real topic, so a plain read of the shared cache is enough. // it here for the file assistant's real topic, so a plain read of the shared cache is enough.
const [bootstrap] = useCachedData(myUserId ? 'bootstrap:' + myUserId : null, getBootstrap, [myUserId]); const [bootstrap] = useCachedData(myUserId ? 'bootstrap:' + myUserId : null, getBootstrap, [myUserId]);
@@ -117,7 +118,7 @@ function AddressBook({myUserId, onTopicSelected}) {
fetchAllPages(cursor => getFriends('', cursor)).then(list => { fetchAllPages(cursor => getFriends('', cursor)).then(list => {
if (friendsCacheKey) { writeCachedData(friendsCacheKey, list); } if (friendsCacheKey) { writeCachedData(friendsCacheKey, list); }
setFriends(list); setFriends(list);
}).catch(() => {}).finally(() => setLoading(false)); }).catch(() => setLoadError(true)).finally(() => setLoading(false));
fetchAllPages(cursor => getFriendRequests('inbound', 'pending', cursor)) fetchAllPages(cursor => getFriendRequests('inbound', 'pending', cursor))
.then(list => setPendingCount(list.length)).catch(() => {}); .then(list => setPendingCount(list.length)).catch(() => {});
}, []); }, []);
@@ -148,7 +149,7 @@ function AddressBook({myUserId, onTopicSelected}) {
<main className="people-scroll"> <main className="people-scroll">
<section className="people-entry-list">{entries.map(item => <button type="button" key={item.label} onClick={() => openEntry(item)}><Avatar icon={item.icon} /><strong>{item.label}</strong>{item.badge ? <em>{item.badge}</em> : null}<Icon>chevron_right</Icon></button>)}</section> <section className="people-entry-list">{entries.map(item => <button type="button" key={item.label} onClick={() => openEntry(item)}><Avatar icon={item.icon} /><strong>{item.label}</strong>{item.badge ? <em>{item.badge}</em> : null}<Icon>chevron_right</Icon></button>)}</section>
<h3 className="people-section-title">好友通讯录</h3> <h3 className="people-section-title">好友通讯录</h3>
{loading ? <p className="people-empty-hint">加载中…</p> : sortedKeys.length === 0 ? <p className="people-empty-hint">暂无好友</p> : null} {loading ? <p className="people-empty-hint">加载中…</p> : sortedKeys.length === 0 ? <p className="people-empty-hint">{loadError ? '通讯录加载失败' : '暂无好友'}</p> : null}
{sortedKeys.map(initial => <section className="people-group" id={'people-group-' + initial} key={initial}><h2>{initial}</h2> {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); }}> {groups[initial].map(person => <button type="button" key={person.user_id} onClick={() => { seedProfile(person); navigate('friend-profile', person.user_id); }}>
{/* 副行只在确实是另一个名字时才显示。后端在有备注时会把 display_name 也设成备注 {/* 副行只在确实是另一个名字时才显示。后端在有备注时会把 display_name 也设成备注
@@ -174,9 +175,10 @@ function requestStatusLabel(item) {
function FriendRequests({onToast}) { function FriendRequests({onToast}) {
const [items, setItems] = React.useState([]); const [items, setItems] = React.useState([]);
const [loading, setLoading] = React.useState(true); const [loading, setLoading] = React.useState(true);
const [loadError, setLoadError] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
fetchAllPages(cursor => getFriendRequests('all', 'all', cursor)) fetchAllPages(cursor => getFriendRequests('all', 'all', cursor))
.then(setItems).catch(() => {}).finally(() => setLoading(false)); .then(setItems).catch(() => setLoadError(true)).finally(() => setLoading(false));
}, []); }, []);
const handle = (item, accept) => { const handle = (item, accept) => {
const nextStatus = accept ? 'accepted' : 'rejected'; const nextStatus = accept ? 'accepted' : 'rejected';
@@ -188,7 +190,7 @@ function FriendRequests({onToast}) {
}); });
}; };
return <div className="people-page"><Header title="新朋友" back="address-book" /><main className="people-request-list"> return <div className="people-page"><Header title="新朋友" back="address-book" /><main className="people-request-list">
{loading ? <p className="people-empty-hint">加载中…</p> : items.length === 0 ? <p className="people-empty-hint">暂无新朋友申请</p> : null} {loading ? <p className="people-empty-hint">加载中…</p> : items.length === 0 ? <p className="people-empty-hint">{loadError ? '加载失败,请重试' : '暂无新朋友申请'}</p> : null}
{items.map(item => <article key={item.request_id}> {items.map(item => <article key={item.request_id}>
<Avatar person={item.peer} /> <Avatar person={item.peer} />
<div><strong>{(item.peer && item.peer.display_name) || item.applicant_id}</strong><p>{item.message || '请求添加您为好友'}</p><small>{new Date(item.created_at).toLocaleString()}</small></div> <div><strong>{(item.peer && item.peer.display_name) || item.applicant_id}</strong><p>{item.message || '请求添加您为好友'}</p><small>{new Date(item.created_at).toLocaleString()}</small></div>
@@ -574,8 +576,9 @@ function Settings({uid, initialBlocked, pinBlocked, onToast}) {
function BlockList({onToast}) { function BlockList({onToast}) {
const [people, setPeople] = React.useState([]); const [people, setPeople] = React.useState([]);
const [loading, setLoading] = React.useState(true); const [loading, setLoading] = React.useState(true);
const [loadError, setLoadError] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
fetchAllPages(cursor => getBlocks(cursor)).then(setPeople).catch(() => {}).finally(() => setLoading(false)); fetchAllPages(cursor => getBlocks(cursor)).then(setPeople).catch(() => setLoadError(true)).finally(() => setLoading(false));
}, []); }, []);
const remove = userId => { const remove = userId => {
unblockUser(userId).then(() => { setPeople(items => items.filter(item => item.user_id !== userId)); onToast('已移出黑名单'); }) unblockUser(userId).then(() => { setPeople(items => items.filter(item => item.user_id !== userId)); onToast('已移出黑名单'); })
@@ -586,7 +589,7 @@ function BlockList({onToast}) {
return <div className="people-page"><Header title="黑名单" back="android-settings" /><main className="people-block-list"> return <div className="people-page"><Header title="黑名单" back="android-settings" /><main className="people-block-list">
{loading ? <p className="people-empty-hint">加载中…</p> : people.length ? {loading ? <p className="people-empty-hint">加载中…</p> : people.length ?
people.map(item => <article key={item.user_id}><Avatar person={item.user} /><span><strong>{(item.user && item.user.display_name) || item.user_id}</strong><small>已阻止消息和通话</small></span><button type="button" onClick={() => remove(item.user_id)}>移出</button></article>) : people.map(item => <article key={item.user_id}><Avatar person={item.user} /><span><strong>{(item.user && item.user.display_name) || item.user_id}</strong><small>已阻止消息和通话</small></span><button type="button" onClick={() => remove(item.user_id)}>移出</button></article>) :
<div className="people-empty"><Icon>person_off</Icon><strong>黑名单为空</strong></div>} <div className="people-empty"><Icon>person_off</Icon><strong>{loadError ? '黑名单加载失败' : '黑名单为空'}</strong></div>}
</main></div>; </main></div>;
} }
+9 -6
View File
@@ -7668,6 +7668,7 @@ function AddressBook({
const [friends, setFriends] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(() => friendsCacheKey && (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.readCachedData)(friendsCacheKey) || []); const [friends, setFriends] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(() => friendsCacheKey && (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.readCachedData)(friendsCacheKey) || []);
const [pendingCount, setPendingCount] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(0); const [pendingCount, setPendingCount] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(0);
const [loading, setLoading] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(() => !(friendsCacheKey && (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.readCachedData)(friendsCacheKey))); const [loading, setLoading] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(() => !(friendsCacheKey && (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.readCachedData)(friendsCacheKey)));
const [loadError, setLoadError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
const [bootstrap] = (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.useCachedData)(myUserId ? 'bootstrap:' + myUserId : null, _lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getBootstrap, [myUserId]); const [bootstrap] = (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.useCachedData)(myUserId ? 'bootstrap:' + myUserId : null, _lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getBootstrap, [myUserId]);
const fileAssistantTopic = bootstrap && bootstrap.entries && bootstrap.entries.file_assistant && bootstrap.entries.file_assistant.topic; const fileAssistantTopic = bootstrap && bootstrap.entries && bootstrap.entries.file_assistant && bootstrap.entries.file_assistant.topic;
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => { react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
@@ -7676,7 +7677,7 @@ function AddressBook({
(0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.writeCachedData)(friendsCacheKey, list); (0,_lib_use_cached_data_js__WEBPACK_IMPORTED_MODULE_5__.writeCachedData)(friendsCacheKey, list);
} }
setFriends(list); setFriends(list);
}).catch(() => {}).finally(() => setLoading(false)); }).catch(() => setLoadError(true)).finally(() => setLoading(false));
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.fetchAllPages)(cursor => (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getFriendRequests)('inbound', 'pending', cursor)).then(list => setPendingCount(list.length)).catch(() => {}); (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.fetchAllPages)(cursor => (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getFriendRequests)('inbound', 'pending', cursor)).then(list => setPendingCount(list.length)).catch(() => {});
}, []); }, []);
const visible = friends.filter(item => `${item.display_name}${item.remark || ''}`.toLowerCase().includes(query.toLowerCase())); const visible = friends.filter(item => `${item.display_name}${item.remark || ''}`.toLowerCase().includes(query.toLowerCase()));
@@ -7756,7 +7757,7 @@ function AddressBook({
children: "加载中…" children: "加载中…"
}, void 0, false) : sortedKeys.length === 0 ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("p", { }, void 0, false) : sortedKeys.length === 0 ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("p", {
className: "people-empty-hint", className: "people-empty-hint",
children: "暂无好友" children: loadError ? '通讯录加载失败' : '暂无好友'
}, void 0, false) : null, sortedKeys.map(initial => (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("section", { }, void 0, false) : null, sortedKeys.map(initial => (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("section", {
className: "people-group", className: "people-group",
id: 'people-group-' + initial, id: 'people-group-' + initial,
@@ -7804,8 +7805,9 @@ function FriendRequests({
}) { }) {
const [items, setItems] = react__WEBPACK_IMPORTED_MODULE_0___default().useState([]); const [items, setItems] = react__WEBPACK_IMPORTED_MODULE_0___default().useState([]);
const [loading, setLoading] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(true); const [loading, setLoading] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(true);
const [loadError, setLoadError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => { react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.fetchAllPages)(cursor => (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getFriendRequests)('all', 'all', cursor)).then(setItems).catch(() => {}).finally(() => setLoading(false)); (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.fetchAllPages)(cursor => (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getFriendRequests)('all', 'all', cursor)).then(setItems).catch(() => setLoadError(true)).finally(() => setLoading(false));
}, []); }, []);
const handle = (item, accept) => { const handle = (item, accept) => {
const nextStatus = accept ? 'accepted' : 'rejected'; const nextStatus = accept ? 'accepted' : 'rejected';
@@ -7834,7 +7836,7 @@ function FriendRequests({
children: "加载中…" children: "加载中…"
}, void 0, false) : items.length === 0 ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("p", { }, void 0, false) : items.length === 0 ? (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("p", {
className: "people-empty-hint", className: "people-empty-hint",
children: "暂无新朋友申请" children: loadError ? '加载失败,请重试' : '暂无新朋友申请'
}, void 0, false) : null, items.map(item => (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("article", { }, void 0, false) : null, 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, { children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Avatar, {
person: item.peer person: item.peer
@@ -8567,8 +8569,9 @@ function BlockList({
}) { }) {
const [people, setPeople] = react__WEBPACK_IMPORTED_MODULE_0___default().useState([]); const [people, setPeople] = react__WEBPACK_IMPORTED_MODULE_0___default().useState([]);
const [loading, setLoading] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(true); const [loading, setLoading] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(true);
const [loadError, setLoadError] = react__WEBPACK_IMPORTED_MODULE_0___default().useState(false);
react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => { react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.fetchAllPages)(cursor => (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getBlocks)(cursor)).then(setPeople).catch(() => {}).finally(() => setLoading(false)); (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.fetchAllPages)(cursor => (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.getBlocks)(cursor)).then(setPeople).catch(() => setLoadError(true)).finally(() => setLoading(false));
}, []); }, []);
const remove = userId => { const remove = userId => {
(0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.unblockUser)(userId).then(() => { (0,_lib_im_api_js__WEBPACK_IMPORTED_MODULE_4__.unblockUser)(userId).then(() => {
@@ -8605,7 +8608,7 @@ function BlockList({
children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Icon, { children: [(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)(Icon, {
children: "person_off" children: "person_off"
}, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("strong", { }, void 0, false), (0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_6__.jsxDEV)("strong", {
children: "黑名单为空" children: loadError ? '黑名单加载失败' : '黑名单为空'
}, void 0, false)] }, void 0, false)]
}, void 0, true) }, void 0, true)
}, void 0, false)] }, void 0, false)]
+1 -1
View File
File diff suppressed because one or more lines are too long