- 桌面布局下打开一个会话时,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>
438 lines
18 KiB
React
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);
|