Files
im_webapp/src/views/sidepanel-view.jsx
T
Your NameandClaude Sonnet 5 38b43936ce 修复桌面端会话内错误提示完全不可见、收藏发送面板显示空白/裸类型名、delivery-marker图标className拼出undefined
- 桌面布局下打开一个会话时,sidepanel-view.jsx 走的是 messages-home 分支(渲染
  MessageCenterView),这个分支从来没有渲染过ErrorPanel;messages-view.jsx自己的ErrorPanel
  又只在displayMobile时才渲染——两边一凑,桌面端(最常见的使用状态)里任何onError(...)调用
  (上传失败、收藏转发失败、URL风控拦截等)全部没有可见的展示出口,用户只会觉得"点了但什么
  反应都没有"。已用真实handleError()调用实测确认:修复前DOM里根本没有.info-box节点,修复后
  正确渲染且visible。
- "我的收藏"发送面板:favoriteSummary()对图片/视频收藏显示空白(真实数据里这两类的
  snapshot.content.txt是占位用的单个空格,被直接当文本展示)、对文本类收藏显示裸露的内部
  类型字符串"text"(FAVORITE_TYPE_LABEL没有text这个key,且文本收藏的content本身就是纯字符串
  而非{txt:...}对象,原逻辑没处理这种形状)。改成图片/视频/语音三类固定用类型标签、文本类型
  优先判断content本身是不是纯字符串、并给FAVORITE_TYPE_LABEL补上text条目。
- received-marker.jsx的已发送/已送达图标(deliveryMarker对这两种状态返回的对象没有color字段)
  拼接className时把undefined直接字符串化拼了进去,实际渲染成"material-icons small undefined"。

均已重新构建验证;received-marker/favoriteSummary为纯函数级修复,逻辑经过仔细核对但未逐一
截图复核(不依赖运行时状态,风险低)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 18:42:08 +08:00

438 lines
18 KiB
React

import React, { Suspense } from 'react';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ErrorPanel from '../widgets/error-panel.jsx';
import LoadSpinner from '../widgets/load-spinner.jsx';
import SideNavbar from '../widgets/side-navbar.jsx';
import TopicCommonView from './topic-common-view.jsx';
import AuthOnboardingView from './auth-onboarding-view.jsx';
import MessageCenterView from './message-center-view.jsx';
import DirectMessageView from './direct-message-view.jsx';
import MessageComposerView, {MESSAGE_COMPOSER_VIEWS} from './message-composer-view.jsx';
import ContactManagementView, {CONTACT_MANAGEMENT_VIEWS} from './contact-management-view.jsx';
import GroupManagementView, {GROUP_MANAGEMENT_VIEWS} from './group-management-view.jsx';
import ContentLibraryView, {CONTENT_LIBRARY_VIEWS} from './content-library-view.jsx';
import CallExperienceView, {CALL_EXPERIENCE_VIEWS} from './call-experience-view.jsx';
import WorkplaceCollaborationView, {WORKPLACE_COLLABORATION_VIEWS} from './workplace-collaboration-view.jsx';
import PersonalSettingsView, {PERSONAL_SETTINGS_VIEWS} from './personal-settings-view.jsx';
import SecuritySupportView, {SECURITY_SUPPORT_VIEWS} from './security-support-view.jsx';
const AccountSettingsView = React.lazy(_ => import('./account-settings-view.jsx'));
import ContactsView from './contacts-view.jsx';
const CreateAccountView = React.lazy(_ => import('./create-account-view.jsx'));
import AccGeneralView from './acc-general-view.jsx';
import AccNotificationsView from './acc-notifications-view.jsx';
const AccSecurityView = React.lazy(_ => import('./acc-security-view.jsx'));
import AccSupportView from './acc-support-view.jsx';
import LoginView from './login-view.jsx';
import NewTopicView from './new-topic-view.jsx';
const PasswordResetView = React.lazy(_ => import('./password-reset-view.jsx'));
import SettingsView from './settings-view.jsx';
import ValidationView from './validation-view.jsx';
const WallpapersView = React.lazy(_ => import('./wallpapers.jsx'));
// Panel titles for translation.
const messages = defineMessages({
login: {
id: 'sidepanel_title_login',
description: 'Sidepanel title for LoginView.',
defaultMessage: 'Sign In'
},
register: {
id: 'sidepanel_title_register',
description: 'Sidepanel title for CreateAccountView.',
defaultMessage: 'Create Account'
},
settings: {
id: 'sidepanel_title_settings',
description: 'Sidepanel title for SettingsView.',
defaultMessage: 'Settings'
},
edit: {
id: 'sidepanel_title_account_settings',
description: 'Sidepanel title for AccountSettingsView.',
defaultMessage: 'Account Settings'
},
general: {
id: 'panel_title_general',
description: 'Title for TopicCommon.',
defaultMessage: 'General'
},
security: {
id: 'panel_title_security',
description: 'Title for TopicSecirity and AccSecurity.',
defaultMessage: 'Security'
},
crop: {
id: 'panel_title_crop',
description: 'Title for AvatarCropView.',
defaultMessage: 'Drag to Adjust'
},
notif: {
id: 'sidepanel_title_acc_notifications',
description: 'Sidepanel title for AccNotificationsView.',
defaultMessage: 'Notifications'
},
acc_general: {
id: 'sidepanel_title_acc_general',
description: 'Sidepanel title for AccGeneralView.',
defaultMessage: 'General'
},
support: {
id: 'sidepanel_title_acc_support',
description: 'Sidepanel title for AccSupportView.',
defaultMessage: 'Support'
},
newtpk: {
id: 'sidepanel_title_newtpk',
description: 'Sidepanel title for NewTopicView.',
defaultMessage: 'Start New Chat'
},
cred: {
id: 'sidepanel_title_cred',
description: 'Sidepanel title for ValidationView.',
defaultMessage: 'Confirm Credentials'
},
reset: {
id: 'sidepanel_title_reset',
description: 'Sidepanel title for PasswordResetView.',
defaultMessage: 'Reset Password'
},
archive: {
id: 'sidepanel_title_archive',
description: 'Sidepanel title for ContactsView-Archive.',
defaultMessage: 'Archived Chats'
},
blocked: {
id: 'sidepanel_title_blocked',
description: 'Sidepanel title for ContactsView-Blocked.',
defaultMessage: 'Blocked Chats'
},
wallpapers: {
id: 'wallpapers',
description: 'Link or title for working with wallpapers.',
defaultMessage: 'Wallpapers'
}
});
class SidepanelView extends React.PureComponent {
constructor(props) {
super(props);
this.handleNewTopic = this.handleNewTopic.bind(this);
}
handleNewTopic() {
this.props.onNavigate('newtpk');
}
render() {
const {formatMessage} = this.props.intl;
const view = this.props.state || (this.props.myUserId ? 'messages-home' : 'tenant');
const chapterOneViews = ['tenant', 'login', 'enterprise-register', 'enterprise-feedback', 'register',
'reset', 'identity-check', 'realname', 'realname-verified', 'realname-reminder', 'independent-password'];
if (chapterOneViews.includes(view)) {
return <div id="sidepanel" className="auth-onboarding-panel">
<AuthOnboardingView
view={view}
hashParams={this.props.hashParams}
authenticated={!!this.props.myUserId}
login={this.props.login}
loginDisabled={this.props.loginDisabled}
persist={this.props.persist}
errorText={this.props.errorText}
errorLevel={this.props.errorLevel}
onLoginRequest={this.props.onLoginRequest}
onPersistenceChange={this.props.onPersistenceChange}
onCreateAccount={this.props.onCreateAccount}
onImRegisterRequest={this.props.onImRegisterRequest}
onTenantResolved={this.props.onTenantResolved}
onError={this.props.onError} />
</div>;
}
const chapterTwoViews = ['messages-home', 'messages-offline', 'messages-menu', 'message-actions', 'system-messages'];
if (chapterTwoViews.includes(view)) {
const fullPage = !this.props.myUserId || view !== 'messages-home';
return <div id="sidepanel" className={'message-center-panel' + (fullPage ? ' full-page' : '')}>
<MessageCenterView
view={view}
key={view}
connected={this.props.connected}
chatList={this.props.chatList}
myUserId={this.props.myUserId}
onTopicSelected={this.props.onTopicSelected}
onOpenScanner={this.props.onOpenScanner} />
{/* This branch never rendered ErrorPanel at all -- on desktop (non-mobile), opening a real
conversation lands here (view stays 'messages-home' behind the split-view topic pane),
and messages-view.jsx's own ErrorPanel is only rendered on displayMobile. Every
this.props.onError(...) call reachable from an open conversation on desktop (failed
uploads, failed forwards, URL risk blocks, etc.) had no visible outlet at all. */}
<ErrorPanel
level={this.props.errorLevel}
text={this.props.errorText}
action={this.props.errorAction}
actionText={this.props.errorActionText}
onClearError={this.props.onError} />
</div>;
}
const chapterThreeViews = ['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'];
if (chapterThreeViews.includes(view)) {
return <div id="sidepanel" className="direct-message-panel">
<DirectMessageView view={view} key={view} />
</div>;
}
if (MESSAGE_COMPOSER_VIEWS.includes(view)) {
return <div id="sidepanel" className="message-composer-panel">
<MessageComposerView view={view} key={view} />
</div>;
}
if (CONTACT_MANAGEMENT_VIEWS.includes(view)) {
return <div id="sidepanel" className="contact-management-panel">
<ContactManagementView view={view} hashParams={this.props.hashParams} myUserId={this.props.myUserId}
tinode={this.props.tinode} onTopicSelected={this.props.onTopicSelected} onOpenScanner={this.props.onOpenScanner} key={view} />
</div>;
}
if (GROUP_MANAGEMENT_VIEWS.includes(view)) {
return <div id="sidepanel" className="group-management-panel">
<GroupManagementView view={view} hashParams={this.props.hashParams} myUserId={this.props.myUserId}
tinode={this.props.tinode} onTopicSelected={this.props.onTopicSelected} key={view} />
</div>;
}
if (CONTENT_LIBRARY_VIEWS.includes(view)) {
return <div id="sidepanel" className="content-library-panel">
<ContentLibraryView view={view} hashParams={this.props.hashParams} myUserId={this.props.myUserId}
tinode={this.props.tinode} onTopicSelected={this.props.onTopicSelected} chatList={this.props.chatList} key={view} />
</div>;
}
if (CALL_EXPERIENCE_VIEWS.includes(view)) {
return <div id="sidepanel" className="call-experience-panel">
<CallExperienceView view={view} key={view} />
</div>;
}
if (WORKPLACE_COLLABORATION_VIEWS.includes(view)) {
return <div id="sidepanel" className="workplace-collaboration-panel">
<WorkplaceCollaborationView view={view} hashParams={this.props.hashParams} myUserId={this.props.myUserId}
tinode={this.props.tinode} onTopicSelected={this.props.onTopicSelected} key={view} />
</div>;
}
if (PERSONAL_SETTINGS_VIEWS.includes(view)) {
return <div id="sidepanel" className="personal-settings-panel">
<PersonalSettingsView view={view} hashParams={this.props.hashParams} myUserId={this.props.myUserId}
tinode={this.props.tinode} chatList={this.props.chatList}
onUpdatePassword={this.props.onUpdatePassword} onDeleteAccount={this.props.onDeleteAccount}
onLogout={this.props.onLogout} key={view} />
</div>;
}
if (SECURITY_SUPPORT_VIEWS.includes(view)) {
return <div id="sidepanel" className="security-support-panel">
<SecuritySupportView view={view} hashParams={this.props.hashParams} tinode={this.props.tinode} key={view} />
</div>;
}
let title, avatar, badges;
if (view == 'contacts') {
title = this.props.title;
avatar = this.props.avatar ? this.props.avatar : true;
badges = this.props.trustedBadges;
} else {
title = formatMessage(messages[view]);
avatar = false;
badges = null;
}
let onCancel;
if (['login', 'contacts'].indexOf(view) == -1) {
onCancel = this.props.onCancel;
}
return (
<div id="sidepanel">
<SideNavbar
state={view}
title={title}
avatar={avatar}
tinode={this.props.tinode}
trustedBadges={badges}
myUserId={this.props.myUserId}
onSignUp={this.props.onSignUp}
onSettings={this.props.onSettings}
onNewTopic={this.handleNewTopic}
onCancel={onCancel} />
<ErrorPanel
level={this.props.errorLevel}
text={this.props.errorText}
action={this.props.errorAction}
actionText={this.props.errorActionText}
onClearError={this.props.onError} />
<LoadSpinner show={this.props.loadSpinnerVisible} />
{view === 'login' ?
<LoginView
login={this.props.login}
disabled={this.props.loginDisabled}
persist={this.props.persist}
onLogin={this.props.onLoginRequest}
onPersistenceChange={this.props.onPersistenceChange} /> :
view === 'register' ?
<Suspense fallback={<div className="panel-form-row"><FormattedMessage id="loading_note"
defaultMessage="Loading..." description="Message shown when component is loading"/></div>}>
<CreateAccountView
tinode={this.props.tinode}
reqCredMethod={this.props.reqCredMethod}
onShowCountrySelector={this.props.onShowCountrySelector}
onCreateAccount={this.props.onCreateAccount}
onCancel={this.props.onCancel}
onError={this.props.onError} />
</Suspense> :
view === 'settings' ?
<SettingsView
transport={this.props.transport}
serverAddress={this.props.serverAddress}
secureConnection={this.props.secureConnection}
onCancel={this.props.onCancel}
onUpdate={this.props.onGlobalSettings} /> :
view === 'edit' ?
<Suspense fallback={<div className="panel-form-row"><FormattedMessage id="loading_note"
defaultMessage="Loading..." description="Message shown when component is loading"/></div>}>
<AccountSettingsView
tinode={this.props.tinode}
myUserId={this.props.myUserId}
trustedBadges={this.props.trustedBadges}
reqCredMethod={this.props.reqCredMethod}
onShowCountrySelector={this.props.onShowCountrySelector}
onNavigate={this.props.onNavigate}
onCredAdd={this.props.onCredAdd}
onCredDelete={this.props.onCredDelete}
onCredConfirm={this.props.onCredConfirm}
onError={this.props.onError} />
</Suspense> :
(view === 'general' || view === 'crop') ?
<TopicCommonView
topic="me"
tinode={this.props.tinode}
myUserId={this.props.myUserId}
reqCredMethod={this.props.reqCredMethod}
onUpdateTopicDesc={this.props.onUpdateAccountDesc}
onUpdateTagsRequest={this.props.onUpdateAccountTags}
onError={this.props.onError} /> :
view === 'acc_general' ?
<AccGeneralView
colorSchema={this.props.colorSchema}
onSelectWallpapers={this.props.onSelectWallpapers}
textSize={this.props.textSize}
sendOnEnter={this.props.sendOnEnter}
onChangeColorSchema={this.props.onChangeColorSchema}
onTextSizeChanged={this.props.onTextSizeChanged}
onSendOnEnterChanged={this.props.onSendOnEnterChanged} /> :
view === 'notif' ?
<AccNotificationsView
messageSounds={this.props.messageSounds}
desktopAlerts={this.props.desktopAlerts}
desktopAlertsEnabled={this.props.desktopAlertsEnabled}
onTogglePushNotifications={this.props.onTogglePushNotifications}
onToggleMessageSounds={this.props.onToggleMessageSounds} /> :
view === 'security' ?
<Suspense fallback={<div className="panel-form-row"><FormattedMessage id="loading_note"
defaultMessage="Loading..." description="Message shown when component is loading"/></div>}>
<AccSecurityView
tinode={this.props.tinode}
incognitoMode={this.props.incognitoMode}
onNavigate={this.props.onNavigate}
onUpdateAccountDesc={this.props.onUpdateAccountDesc}
onUpdatePassword={this.props.onUpdatePassword}
onLogout={this.props.onLogout}
onDeleteAccount={this.props.onDeleteAccount}
onShowAlert={this.props.onShowAlert}
onShowBlocked={this.props.onShowBlocked}
onToggleIncognitoMode={this.props.onToggleIncognitoMode} />
</Suspense> :
view === 'support' ?
<AccSupportView
serverAddress={this.props.serverAddress}
serverVersion={this.props.serverVersion} /> :
view === 'wallpapers' ?
<Suspense fallback={<div className="panel-form-row"><FormattedMessage id="loading_note"
defaultMessage="Loading..." description="Message shown when component is loading"/></div>}>
<WallpapersView
wallpaper={this.props.wallpaper}
wallpaperSize={this.props.wallpaperSize}
wallpaperBlur={this.props.wallpaperBlur}
colorSchema={this.props.colorSchema !== 'auto' ? this.props.colorSchema : this.props.systemColorSchema}
onWallpaperSelected={this.props.onWallpaperSelected}
/>
</Suspense> :
(view === 'contacts' || view == 'archive' || view == 'blocked') ?
<ContactsView
tinode={this.props.tinode}
myUserId={this.props.myUserId}
connected={this.props.connected}
topicSelected={this.props.topicSelected}
archive={view == 'archive'}
blocked={view == 'blocked'}
chatList={this.props.chatList}
showContextMenu={this.props.showContextMenu}
onTopicSelected={this.props.onTopicSelected}
onShowArchive={this.props.onShowArchive} /> :
view === 'newtpk' ?
<NewTopicView
tinode={this.props.tinode}
searchResults={this.props.searchResults}
onInitFind={this.props.onInitFind}
onSearchContacts={this.props.onSearchContacts}
onCreateTopic={this.props.onCreateTopic}
onError={this.props.onError} /> :
view === 'cred' ?
<ValidationView
credCode={this.props.credCode}
credMethod={this.props.credMethod}
credToken={this.props.credToken}
onSubmit={this.props.onValidateCredentials}
onCancel={this.props.onCancel} /> :
view === 'reset' ?
<Suspense fallback={<div className="panel-form-row"><FormattedMessage id="loading_note"
defaultMessage="Loading..." description="Message shown when component is loading"/></div>}>
<PasswordResetView
tinode={this.props.tinode}
reqCredMethod={this.props.reqCredMethod}
onShowCountrySelector={this.props.onShowCountrySelector}
onRequest={this.props.onPasswordResetRequest}
onReset={this.props.onResetPassword}
onCancel={this.props.onCancel}
onError={this.props.onError} />
</Suspense> :
null}
</div>
);
}
};
export default injectIntl(SidepanelView);