修复置顶/免打扰开关未接后端、好友二维码死按钮(第5章复测发现)
置顶聊天/消息免打扰:原来是纯本地 useState,onChange 只切一下布尔值,从不调用任何接口——
GET/PATCH /conversations/{topic}/settings 早就封装好了,只是零调用点。打开设置页永远显示
"关",点了也只是那次会话内生效、离开页面就丢,和会话列表真正读取的置顶/免打扰状态是两套
毫不相关的东西。
实测确认真实字段:{pinned, mute_notifications, version},PATCH 需要带 version 做乐观锁。
接入 getConversationSettings/updateConversationSettings,失败时回滚开关视觉状态。
已用真实账号验证:点击后发出真实 PATCH 请求,带正确的 version。
好友个人主页"二维码"图标:参考图里是独立一行(不是贴在头像卡片里的小图标),点击生成这个
好友的二维码名片,对应接口 createUserQRCode() 早就封装好了,原来这个图标没有任何 onClick,
是个看着能点、点了没反应的死装饰。新增 friend-qr 路由和 FriendQr 组件(复用 QrPage 的结构),
CSS 复用了 .people-settings 那套现成的行样式,不是新造一套。
已用真实账号验证:点击后正确跳转、生成出真实二维码。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d3d7e11b08
commit
0dcef54fed
@@ -6,7 +6,8 @@ import HashNavigation from '../lib/navigation.js';
|
||||
import {
|
||||
getFriends, getFriendRequests, createFriendRequest, acceptFriendRequest, rejectFriendRequest,
|
||||
deleteFriend, updateFriendRemark, searchUsers, getUserProfile, createMyQRCode, getBlocks,
|
||||
blockUser, unblockUser, getFileUrl, getBootstrap
|
||||
blockUser, unblockUser, getFileUrl, getBootstrap, getConversationSettings, updateConversationSettings,
|
||||
createUserQRCode
|
||||
} from '../lib/im-api.js';
|
||||
import { readCachedData, writeCachedData, useCachedData } from '../lib/use-cached-data.js';
|
||||
|
||||
@@ -19,7 +20,7 @@ const navigate = (view, uid) => {
|
||||
};
|
||||
|
||||
export const CONTACT_MANAGEMENT_VIEWS = [
|
||||
'address-book', 'friend-requests', 'add-friend', 'my-friend-qr', 'friend-search-result',
|
||||
'address-book', 'friend-requests', 'add-friend', 'my-friend-qr', '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'
|
||||
@@ -313,6 +314,27 @@ function QrPage({onToast}) {
|
||||
</main></div>;
|
||||
}
|
||||
|
||||
function FriendQr({uid, onToast}) {
|
||||
const [payload, setPayload] = React.useState(null);
|
||||
const [profile, setProfile] = React.useState(null);
|
||||
React.useEffect(() => {
|
||||
if (!uid) return;
|
||||
createUserQRCode(uid).then(data => { setPayload(data.qr_payload); setProfile(data.display_profile); }).catch(() => onToast('二维码生成失败'));
|
||||
}, [uid]);
|
||||
const save = () => {
|
||||
const canvas = document.querySelector('.people-qrcode canvas');
|
||||
if (!canvas) { onToast('二维码尚未生成'); return; }
|
||||
const a = document.createElement('a');
|
||||
a.href = canvas.toDataURL('image/png');
|
||||
a.download = '好友二维码.png';
|
||||
a.click();
|
||||
onToast('二维码已保存到相册');
|
||||
};
|
||||
return <div className="people-page people-qr-page"><Header title="二维码" back="friend-profile" backUid={uid} /><main><QrCard payload={payload} profile={profile} loading={!payload} />
|
||||
<div className="people-qr-actions"><button type="button" onClick={save}><Icon>download</Icon><span>保存到相册</span></button></div>
|
||||
</main></div>;
|
||||
}
|
||||
|
||||
function Verification({uid, onToast}) {
|
||||
// Same seed-then-best-effort-refresh as Profile -- reached right after it (via its "添加好友"
|
||||
// button), so the cache this reads is the one Profile itself just kept warm.
|
||||
@@ -391,7 +413,13 @@ function Profile({uid, friend, onTopicSelected, onToast}) {
|
||||
<main className="people-profile">
|
||||
<section><Avatar person={profile} /><div><h2>{profile.display_name}</h2>
|
||||
{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></section>
|
||||
{/* 参考图 好友个人主页.png 里"二维码"是独立一行(不是贴在头像卡片里的小图标),点击生成
|
||||
这个好友的二维码名片。对应接口 createUserQRCode() 早就封装好了,原来这个图标没有任何
|
||||
onClick,是个看着能点、点了没反应的死装饰。 */}
|
||||
<button type="button" className="people-profile-qr-row" onClick={() => navigate('friend-qr', uid)}>
|
||||
<span>二维码</span><Icon>qr_code</Icon>
|
||||
</button>
|
||||
{/* 后端存的是 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}
|
||||
@@ -415,6 +443,12 @@ function Settings({uid, initialBlocked, pinBlocked, onToast}) {
|
||||
// blank instead of just stuck like Profile, but no less wrong.
|
||||
const [profile, setProfile] = React.useState(() => (uid ? readCachedData('profile-seed:' + uid) : null));
|
||||
const [blocked, setBlocked] = React.useState(initialBlocked);
|
||||
// 置顶/免打扰原来是纯本地 useState,onChange 只是切一下布尔值,从不调用任何接口——
|
||||
// GET/PATCH /conversations/{topic}/settings 早就封装好了(getConversationSettings/
|
||||
// updateConversationSettings),只是零调用点。实测接口返回真实的 {pinned, mute_notifications,
|
||||
// version},PATCH 需要带 version 做乐观锁。打开设置页永远显示"关",点了也只是那次会话内
|
||||
// 生效、离开页面就丢,和会话列表真正读取的置顶/免打扰状态是两套毫不相关的东西。
|
||||
const [settingsVersion, setSettingsVersion] = React.useState(null);
|
||||
const [pinned, setPinned] = React.useState(false);
|
||||
const [muted, setMuted] = React.useState(false);
|
||||
const [editingRemark, setEditingRemark] = React.useState(false);
|
||||
@@ -422,6 +456,9 @@ function Settings({uid, initialBlocked, pinBlocked, onToast}) {
|
||||
const [remarkDraft, setRemarkDraft] = React.useState('');
|
||||
React.useEffect(() => {
|
||||
if (!uid) return;
|
||||
getConversationSettings(uid).then(s => {
|
||||
setPinned(!!s.pinned); setMuted(!!s.mute_notifications); setSettingsVersion(s.version);
|
||||
}).catch(() => {});
|
||||
// pinBlocked:确认弹窗把这个页面当背景渲染,并用 initialBlocked 摆出"操作之后"的样子。
|
||||
// 这时不能让这次拉取回来的真实关系(还是 friend)把它改回去——原来会导致弹窗一弹出来,
|
||||
// 背后的拉黑开关自己跳回"未拉黑",同时"删除好友"红条又冒出来,跟参考图不符也跟弹窗
|
||||
@@ -450,8 +487,16 @@ function Settings({uid, initialBlocked, pinBlocked, onToast}) {
|
||||
<span>查看聊天记录</span><Icon>chevron_right</Icon>
|
||||
</button>
|
||||
<div><span>消息传输方式</span><small>已启动加密传输</small></div></section>
|
||||
<section><div><span>置顶聊天</span><Toggle checked={pinned} onChange={setPinned} /></div>
|
||||
<div><span>消息免打扰</span><Toggle checked={muted} onChange={setMuted} /></div>
|
||||
<section><div><span>置顶聊天</span><Toggle checked={pinned} onChange={value => {
|
||||
const prev = pinned; setPinned(value);
|
||||
updateConversationSettings(uid, {pinned: value, version: settingsVersion}).then(s => setSettingsVersion(s.version))
|
||||
.catch(err => { setPinned(prev); onToast(err.message || '设置失败'); });
|
||||
}} /></div>
|
||||
<div><span>消息免打扰</span><Toggle checked={muted} onChange={value => {
|
||||
const prev = muted; setMuted(value);
|
||||
updateConversationSettings(uid, {mute_notifications: value, version: settingsVersion}).then(s => setSettingsVersion(s.version))
|
||||
.catch(err => { setMuted(prev); onToast(err.message || '设置失败'); });
|
||||
}} /></div>
|
||||
<button type="button" onClick={() => navigate('clear-chat-confirm', uid)}><span>清空聊天记录</span><Icon>chevron_right</Icon></button>
|
||||
{editingRemark ?
|
||||
<div className="people-remark-edit"><input value={remarkDraft} maxLength="20" onChange={e => setRemarkDraft(e.target.value)} autoFocus /><button type="button" onClick={saveRemark}>保存</button></div> :
|
||||
@@ -532,6 +577,7 @@ export default function ContactManagementView({view, hashParams, myUserId, tinod
|
||||
else if (view === 'friend-requests') content = <FriendRequests onToast={setToast} />;
|
||||
else if (view === 'add-friend' || view === 'friend-search-result') content = <AddFriend result={view === 'friend-search-result'} onToast={setToast} onOpenScanner={onOpenScanner} />;
|
||||
else if (view === 'my-friend-qr') content = <QrPage onToast={setToast} />;
|
||||
else if (view === 'friend-qr') content = <FriendQr uid={uid} onToast={setToast} />;
|
||||
else if (view === 'friend-verification') content = <Verification uid={uid} onToast={setToast} />;
|
||||
else if (view === 'friend-profile' || view === 'group-member-profile') content = <Profile uid={uid} friend={view === 'friend-profile'} onTopicSelected={onTopicSelected} onToast={setToast} />;
|
||||
else if (view === 'friend-settings' || view === 'friend-settings-blocked') content = <Settings uid={uid} initialBlocked={view === 'friend-settings-blocked'} onToast={setToast} />;
|
||||
|
||||
@@ -118,7 +118,7 @@ const SIDE_PANEL_VIEWS = ['acc_general','archive','blocked','contacts','cred','c
|
||||
'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',
|
||||
'address-book','friend-requests','add-friend','my-friend-qr','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',
|
||||
@@ -2522,7 +2522,7 @@ class TinodeWeb extends React.Component {
|
||||
'media-uploading', 'media-gallery', 'media-retry', 'upload-file', 'file-message',
|
||||
'message-multiselect', 'favorite-success', 'forward-select', 'forward-multiple',
|
||||
'forward-individual', 'forward-bundle'].includes(activeProductView);
|
||||
const chapterFiveActive = ['address-book', 'friend-requests', 'add-friend', 'my-friend-qr',
|
||||
const chapterFiveActive = ['address-book', 'friend-requests', 'add-friend', 'my-friend-qr', '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'].includes(activeProductView);
|
||||
|
||||
Reference in New Issue
Block a user