a ton more refactoring

This commit is contained in:
or-else
2021-08-07 15:34:22 -07:00
parent 6d4ff9d625
commit fcb61dcc3b
27 changed files with 1173 additions and 928 deletions
+47 -37
View File
@@ -4,9 +4,6 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import Tinode from 'tinode-sdk';
import TopicCommonView from './topic-common-view.jsx';
import TopicSecurityView from './topic-security-view.jsx';
import AvatarUpload from '../widgets/avatar-upload.jsx';
import BadgeList from '../widgets/badge-list.jsx';
import CheckBox from '../widgets/checkbox.jsx';
@@ -18,6 +15,8 @@ import MenuCancel from '../widgets/menu-cancel.jsx';
import MoreButton from '../widgets/more-button.jsx';
import PermissionsEditor from '../widgets/permissions-editor.jsx';
import TagManager from '../widgets/tag-manager.jsx';
import TopicCommon from '../widgets/topic-common.jsx';
import TopicSecurity from '../widgets/topic-security.jsx';
import { MAX_TITLE_LENGTH, MAX_TOPIC_DESCRIPTION_LENGTH, NO_ACCESS_MODE } from '../config.js';
@@ -26,20 +25,25 @@ import { arrayEqual, theCard } from '../lib/utils.js';
const messages = defineMessages({
info: {
id: 'title_info',
id: 'panel_title_info',
description: 'Title for InfoView',
defaultMessage: 'Info'
},
general: {
id: 'panel_title_acc_general',
description: 'Title for TopicCommonView.',
id: 'panel_title_general',
description: 'Title for TopicCommon.',
defaultMessage: 'General'
},
security: {
id: 'panel_title_acc_security',
description: 'Title for TopicSecirityView and AccSecurityView.',
id: 'panel_title_security',
description: 'Title for TopicSecirity and AccSecurity.',
defaultMessage: 'Security'
},
members: {
id: 'panel_title_members',
description: 'Title for managing group members view.',
defaultMessage: 'Members'
},
crop: {
id: 'panel_title_crop',
description: 'Title for AvatarCropView.',
@@ -75,7 +79,6 @@ class InfoView extends React.Component {
owner: false,
admin: false,
sharer: false,
deleter: false,
muted: false,
address: null,
groupTopic: undefined,
@@ -94,7 +97,6 @@ class InfoView extends React.Component {
anon: null,
contactList: [],
trustedBadges: [],
showMemberPanel: false,
showPermissionEditorFor: undefined,
previousMetaDesc: undefined,
previousSubsUpdated: undefined,
@@ -112,7 +114,6 @@ class InfoView extends React.Component {
this.handleLaunchPermissionsEditor = this.handleLaunchPermissionsEditor.bind(this);
this.handleHidePermissionsEditor = this.handleHidePermissionsEditor.bind(this);
this.handleShowAddMembers = this.handleShowAddMembers.bind(this);
this.handleHideAddMembers = this.handleHideAddMembers.bind(this);
this.handleMemberUpdateRequest = this.handleMemberUpdateRequest.bind(this);
this.handleMemberSelected = this.handleMemberSelected.bind(this);
this.handleContextMenu = this.handleContextMenu.bind(this);
@@ -189,7 +190,6 @@ class InfoView extends React.Component {
owner: acs && acs.isOwner(),
admin: acs && acs.isAdmin(),
sharer: acs && acs.isSharer(),
deleter: acs && acs.isDeleter(),
muted: acs && acs.isMuted(),
fullName: _clip(topic.public ? topic.public.fn : undefined, MAX_TITLE_LENGTH),
@@ -200,7 +200,6 @@ class InfoView extends React.Component {
address: topic.name,
groupTopic: topic.isGroupType(),
channel: topic.isChannelType(),
showMemberPanel: false,
access: acs ? acs.getMode() : undefined,
modeGiven: acs ? acs.getGiven() : undefined,
modeWant: acs ? acs.getWant() : undefined,
@@ -264,8 +263,8 @@ class InfoView extends React.Component {
this.props.onChangePermissions(this.props.topic, checked ? '-P' : '+P');
}
handlePermissionsChanged(perm) {
switch (this.state.showPermissionEditorFor) {
handlePermissionsChanged(which, perm) {
switch (which) {
case 'auth':
this.props.onTopicDescUpdate(this.props.topic, null, null, {auth: perm});
break;
@@ -348,10 +347,9 @@ class InfoView extends React.Component {
}
default:
console.log("Unknown permission editing mode '" + which + "'");
break;
return;
}
this.setState({
showPermissionEditorFor: which,
userPermissionsEdited: uid,
userPermissionsTitle: userTitle,
userPermissionsAvatar: userAvatar,
@@ -361,25 +359,22 @@ class InfoView extends React.Component {
immutablePermissionsTitle: titleCompare,
editedPermissionsSkipped: toSkip,
});
this.props.onNavigate(`perm/${which}`);
}
handleHidePermissionsEditor() {
this.setState({showPermissionEditorFor: undefined});
this.props.onNavigate('info');
}
handleShowAddMembers(e) {
e.preventDefault();
this.props.onInitFind();
this.setState({showMemberPanel: true});
}
handleHideAddMembers() {
this.setState({showMemberPanel: false});
this.props.onNavigate('members');
}
handleMemberUpdateRequest(members, added, removed) {
this.props.onMemberUpdateRequest(this.props.topic, added, removed);
this.setState({showMemberPanel: false});
this.props.onNavigate('info');
}
handleMemberSelected(uid) {
@@ -389,6 +384,8 @@ class InfoView extends React.Component {
handleBackNavigate() {
if (this.props.panel == 'info') {
this.props.onNavigate(null);
} else if ((this.props.panel || '').startsWith('perm/')) {
this.props.onNavigate('security');
} else {
this.props.onNavigate('info');
}
@@ -422,7 +419,10 @@ class InfoView extends React.Component {
}
render() {
const view = this.props.panel || 'info';
const args = (this.props.panel || 'info').split('/');
const view = args[0];
args.shift();
const {formatMessage} = this.props.intl;
const panelTitle = formatMessage(messages[view] || messages['info']);
@@ -439,17 +439,17 @@ class InfoView extends React.Component {
level={this.props.errorLevel}
text={this.props.errorText}
onClearError={this.props.onError} /> : null}
{this.state.showMemberPanel ?
{view == 'members' ?
<GroupManager
members={this.state.contactList}
requiredMember={this.props.myUserId}
keepInitialMembers={!this.state.admin && !this.state.owner}
myUserId={this.props.myUserId}
contacts={this.props.searchableContacts}
onCancel={this.handleHideAddMembers}
onCancel={this.handleBackNavigate}
onSubmit={this.handleMemberUpdateRequest} />
:
this.state.showPermissionEditorFor ?
view == 'perm' && args.length > 0 ?
<PermissionsEditor
mode={this.state.editedPermissions}
compare={this.state.immutablePermissions}
@@ -459,21 +459,31 @@ class InfoView extends React.Component {
userTitle={this.state.userPermissionsTitle}
item={this.state.userPermissionsEdited}
userAvatar={this.state.userPermissionsAvatar}
onSubmit={this.handlePermissionsChanged}
onCancel={this.handleHidePermissionsEditor}
/>
onSubmit={this.handlePermissionsChanged.bind(...args)}
onCancel={this.handleHidePermissionsEditor} />
:
this.props.panel == 'general' ?
<TopicCommonView
view == 'general' ?
<TopicCommon
tinode={this.props.tinode}
topic={this.props.topic}
/>
onCredAdd={this.props.onCredAdd}
onTopicTagsUpdate={this.props.onTopicTagsUpdate}
onCredConfirm={this.props.onCredConfirm}
onCredDelete={this.props.onCredDelete}
onUpdateTopicDesc={this.props.onUpdateTopicDesc}
onUpdateTags={this.props.onUpdateTags}
onError={this.props.onError} />
:
this.props.panel == 'security' ?
<TopicSecurityView
view == 'security' ?
<TopicSecurity
tinode={this.props.tinode}
topic={this.props.topic}
/>
onShowAlert={this.props.onShowAlert}
onDeleteMessages={this.props.onDeleteMessages}
onLeaveTopic={this.props.onLeaveTopic}
onBlockTopic={this.props.onBlockTopic}
onReportTopic={this.props.onReportTopic}
onNavigate={this.props.onNavigate} />
:
<div id="info-view-content" className="scrollable-panel">
<div className="panel-form-column">
+2 -2
View File
@@ -63,9 +63,9 @@ class NewTopicView extends React.Component {
}
}
handleNewGroupSubmit(name, dataUrl, priv, tags, isChannel) {
handleNewGroupSubmit(name, description, dataUrl, priv, tags, isChannel) {
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
this.props.onCreateTopic(undefined, theCard(name, dataUrl), priv, tags, isChannel);
this.props.onCreateTopic(undefined, theCard(name, dataUrl, null, description), priv, tags, isChannel);
}
handleGroupByID(topicName) {
+6 -6
View File
@@ -4,11 +4,11 @@ import { defineMessages, 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 TopicCommon from '../widgets/topic-common.jsx';
import AccountSettingsView from './account-settings-view.jsx';
import ContactsView from './contacts-view.jsx';
import CreateAccountView from './create-account-view.jsx';
import TopicCommonView from './topic-common-view.jsx';
import AccNotificationsView from './acc-notifications-view.jsx';
import AccSecurityView from './acc-security-view.jsx';
import AccSupportView from './acc-support-view.jsx';
@@ -41,13 +41,13 @@ const messages = defineMessages({
defaultMessage: 'Account Settings'
},
general: {
id: 'panel_title_acc_general',
description: 'Title for TopicCommonView.',
id: 'panel_title_general',
description: 'Title for TopicCommon.',
defaultMessage: 'General'
},
security: {
id: 'panel_title_acc_security',
description: 'Title for TopicSecirityView and AccSecurityView.',
id: 'panel_title_security',
description: 'Title for TopicSecirity and AccSecurity.',
defaultMessage: 'Security'
},
crop: {
@@ -176,7 +176,7 @@ class SidepanelView extends React.Component {
onNavigate={this.props.onNavigate} /> :
(view === 'general' || view === 'crop') ?
<TopicCommonView
<TopicCommon
topic="me"
tinode={this.props.tinode}
myUserId={this.props.myUserId}
-199
View File
@@ -1,199 +0,0 @@
// View for editing topic (including 'me' topic) parameters.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import Tinode from 'tinode-sdk';
import AvatarUpload from '../widgets/avatar-upload.jsx';
import InPlaceEdit from '../widgets/in-place-edit.jsx';
import TagManager from '../widgets/tag-manager.jsx';
import TopicDescEdit from '../widgets/topic-desc-edit.jsx';
import { AVATAR_SIZE, MAX_AVATAR_BYTES, MAX_EXTERN_ATTACHMENT_SIZE,
MAX_INBAND_ATTACHMENT_SIZE, MAX_TITLE_LENGTH } from '../config.js';
import { imageScaled, blobToBase64, makeImageUrl } from '../lib/blob-helpers.js';
import { arrayEqual, asEmail, asPhone, theCard } from '../lib/utils.js';
export default class TopicCommonView extends React.Component {
constructor(props) {
super(props);
const topic = this.props.tinode.getTopic(this.props.topic);
const acs = topic.getAccessMode();
this.state = {
isMe: this.props.tinode.isMe(this.props.topic),
owner: acs && acs.isOwner(),
credentials: topic.getCredentials() || [],
addCredActive: false,
addCredInvalid: false,
tags: [],
previousTagsUpdated: undefined
};
this.onTagsUpdated = this.onTagsUpdated.bind(this);
this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
this.tnCredsUpdated = this.tnCredsUpdated.bind(this);
this.handleCredChange = this.handleCredChange.bind(this);
this.handleCredKeyDown = this.handleCredKeyDown.bind(this);
this.handleCredEntered = this.handleCredEntered.bind(this);
}
// No need to separately handle component mount.
componentDidUpdate(props) {
const topic = this.props.tinode.getTopic(props.topic);
if (!topic) {
return;
}
topic.onCredsUpdated = this.tnCredsUpdated;
if (topic.onTagsUpdated != this.onTagsUpdated) {
if (topic.getType() == 'grp') {
this.previousTagsUpdated = topic.onTagsUpdated;
topic.onTagsUpdated = this.onTagsUpdated;
} else {
this.previousTagsUpdated = undefined;
}
}
if (this.state.topic != props.topic) {
this.setState({topic: props.topic});
}
}
componentWillUnmount() {
const topic = this.props.tinode.getTopic(this.props.topic);
topic.onCredsUpdated = undefined;
topic.onTagsUpdated = this.previousTagsUpdated;
}
tnCredsUpdated(creds) {
this.setState({credentials: creds || []});
}
handleCredChange(e) {
this.setState({newCred: e.target.value, addCredInvalid: false});
}
handleCredKeyDown(e) {
if (e.keyCode === 27) {
// Escape pressed
this.setState({newCred: '', addCredActive: false});
} else if (e.keyCode === 13) {
// Enter pressed
this.handleCredEntered(e);
}
}
handleCredEntered(e) {
const value = this.state.newCred.trim();
if (!value) {
this.setState({addCredActive: false, addCredInvalid: false});
return;
}
let val = asPhone(value);
let method;
if (val) {
method = 'tel';
} else {
val = asEmail(value);
if (val) {
method = 'email';
}
}
if (method) {
this.props.onCredAdd(method, val);
this.setState({addCredActive: false, newCred: ''});
} else {
this.setState({addCredInvalid: true});
}
}
onTagsUpdated(tags) {
this.setState({tags: tags});
if (this.previousTagsUpdated && this.previousTagsUpdated != this.onTagsUpdated) {
this.previousTagsUpdated();
}
}
handleTagsUpdated(tags) {
if (!arrayEqual(this.state.tags.slice(0), tags.slice(0))) {
this.props.onTopicTagsUpdate(this.props.topic, tags);
}
}
render() {
const credentials = [];
if (this.state.isMe) {
this.state.credentials.map((cred) => {
credentials.push(
<div key={cred.meth + ":" + cred.val + ":" + cred.done}>{cred.meth}: <tt>{cred.val}</tt>
<span> {!cred.done ?
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCredConfirm(cred.meth, cred.val);}}>
<FormattedMessage id="validate_credential_action" defaultMessage="confirm"
description="Validate credentail call to action" />
</a>
: null} <a href="#" onClick={(e) => {e.preventDefault(); this.props.onCredDelete(cred.meth, cred.val);}}><i
className="material-icons gray">delete_outline</i></a></span>
</div>
);
});
}
return (
<div className="scrollable-panel">
<TopicDescEdit
tinode={this.props.tinode}
topic={this.props.topic}
onUpdateTopicDesc={this.props.onUpdateTopicDesc}
onUpdateTags={this.props.onUpdateTags}
onError={this.props.onError} />
{this.state.isMe ?
<>
<div className="hr" />
<div className="panel-form-column">
<label className="small">
<FormattedMessage id="label_user_contacts" defaultMessage="Contacts:"
description="Label for user contacts" />
</label>
<div className="quoted">
{credentials}
{this.state.addCredActive ?
<input type="text" value={this.state.value}
className={this.state.addCredInvalid ? 'invalid' : null}
placeholder="Phone number or email" required="required" autoFocus
onChange={this.handleCredChange} onKeyDown={this.handleCredKeyDown} onBlur={this.handleCredEntered} />
: null}
<div>
<a href="#" onClick={(e) => {e.preventDefault(); this.setState({addCredActive: true});}}>
<i className="material-icons">add</i>
<FormattedMessage id="button_add_another" defaultMessage="Add another" description="Call to action [+ add another]" />
</a></div>
</div>
</div>
</>
: null
}
{this.state.owner ?
<>
<div className="hr" />
<FormattedMessage id="title_tag_manager" defaultMessage="Tags (user discovery)"
description="Section title for TagManager">{
(tags) => <TagManager
title={tags}
tags={this.state.tags}
activated={false}
tinode={this.props.tinode}
onSubmit={this.handleTagsUpdated} />
}</FormattedMessage>
</>
:
null
}
</div>
);
}
};
-267
View File
@@ -1,267 +0,0 @@
// Edit account parameters.
import React from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import InPlaceEdit from '../widgets/in-place-edit.jsx';
import PermissionsEditor from '../widgets/permissions-editor.jsx';
const messages = defineMessages({
clear_messages: {
id: 'action_clear_messages',
defaultMessage: 'Clear Messages',
description: 'Flat button [Clear Messages] (soft-delete messages)'
},
clear_messages_warning: {
id: 'clear_messages_warning',
defaultMessage: 'Are you sure you want to clear all messages? It cannot be undone.',
description: 'Alert dialog warning when deleting all messages.'
},
delete_messages: {
id: 'action_delete_messages',
defaultMessage: 'Clear Messages for All',
description: 'Flat button [Clear for All] (hard-delete all messages)'
},
delete_messages_warning: {
id: 'delete_messages_warning',
defaultMessage: 'Are you sure you want to delete all messages for everyone? It cannot be undone.',
description: 'Alert dialog warning when hard-deleting all messages.'
},
leave_chat: {
id: 'action_leave_chat',
defaultMessage: 'Leave Conversation',
description: 'Flat button [Leave Conversation]'
},
leave_chat_warning: {
id: 'leave_chat_warning',
defaultMessage: 'Are you sure you want to leave this conversation?',
description: 'Alert dialog warning when unsubscribing from a chat.'
},
block_contact: {
id: 'action_block_contact',
defaultMessage: "Block Contact",
description: "Flat button [Block Contact]"
},
block_contact_warning: {
id: 'block_contact_warning',
defaultMessage: 'Are you sure you want to block this contact?',
description: 'Alert dialog warning when blocking a contact.'
},
report_chat: {
id: 'action_report_chat',
defaultMessage: 'Report Conversation',
description: 'Flat button [Report Group]'
},
report_chat_warning: {
id: 'report_chat_warning',
defaultMessage: 'Are you sure you want to block and report this conversation?',
description: 'Alert dialog warning when reporting a conversation for abuse'
},
other_user: {
id: 'label_other_user',
defaultMessage: 'Other',
description: 'Label for the other user when the user is unnamed'
},
});
class TopicSecurityView extends React.Component {
constructor(props) {
super(props);
const topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
const defacs = topic.getDefaultAccess() || {};
const acs = topic.getAccessMode();
this.state = {
owner: acs && acs.isOwner(),
admin: acs && acs.isAdmin(),
sharer: acs && acs.isSharer(),
deleter: acs && acs.isDeleter(),
muted: acs && acs.isMuted(),
auth: defacs ? defacs.auth : null,
anon: defacs ? defacs.anon : null,
showPermissionEditorFor: undefined
};
this.handleLaunchPermissionsEditor = this.handleLaunchPermissionsEditor.bind(this);
this.handleHidePermissionsEditor = this.handleHidePermissionsEditor.bind(this);
this.handlePermissionsChanged = this.handlePermissionsChanged.bind(this);
this.handleDeleteMessages = this.handleDeleteMessages.bind(this);
this.handleLeave = this.handleLeave.bind(this);
this.handleBlock = this.handleBlock.bind(this);
this.handleReport = this.handleReport.bind(this);
}
handleLaunchPermissionsEditor(which) {
this.setState({
showPermissionEditorFor: which,
editedPermissions: this.state[which]
});
}
handleHidePermissionsEditor() {
this.setState({showPermissionEditorFor: undefined});
}
handlePermissionsChanged(perm) {
let defacs = {};
defacs[this.state.showPermissionEditorFor] = perm;
this.props.onUpdateAccountDesc('me', undefined, undefined, defacs);
let newState = {showPermissionEditorFor: undefined};
newState[this.state.showPermissionEditorFor] = perm;
this.setState(newState);
}
handleDeleteMessages(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
this.props.onShowAlert(
formatMessage(this.state.deleter ? messages.delete_messages : messages.clear_messages), // title
formatMessage(this.state.deleter ? messages.delete_messages_warning : messages.clear_messages_warning), // content
(() => { this.props.onDeleteMessages(this.props.topic); }), // onConfirm
null, // "OK"
true, // Show Reject button
null // "Cancel"
);
}
handleLeave(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
this.props.onShowAlert(
formatMessage(messages.leave_chat), // title
formatMessage(messages.leave_chat_warning), // content
(() => { this.props.onLeaveTopic(this.props.topic); }), // onConfirm
null, // "OK"
true, // Show Reject button
null // "Cancel"
);
}
handleBlock(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
this.props.onShowAlert(
formatMessage(messages.block_contact), // title
formatMessage(messages.block_contact_warning), // content
(() => { this.props.onBlockTopic(this.props.topic); }), // onConfirm
null, // "OK"
true, // Show Reject button
null // "Cancel"
);
}
handleReport(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
this.props.onShowAlert(
formatMessage(messages.report_chat), // title
formatMessage(messages.report_chat_warning), // content
(() => { this.props.onReportTopic(this.props.topic); }), // onConfirm
null, // "OK"
true, // Show Reject button
null // "Cancel"
);
}
render() {
const {formatMessage} = this.props.intl;
return (
this.state.showPermissionEditorFor ?
<PermissionsEditor
mode={this.state.editedPermissions}
skip="O"
onSubmit={this.handlePermissionsChanged}
onCancel={this.handleHidePermissionsEditor} />
:
<div className="scrollable-panel">
<div className="panel-form-column">
{!this.state.channel ?
<a href="#" className="flat-button" onClick={this.handleDeleteMessages}>
<i className="material-icons">delete_outline</i> &nbsp;{
formatMessage(this.state.deleter ? messages.delete_messages : messages.clear_messages)
}
</a>
:
null
}
<a href="#" className="danger flat-button" onClick={this.handleLeave}>
<i className="material-icons">exit_to_app</i> &nbsp;{formatMessage(messages.leave_chat)}
</a>
{!this.state.groupTopic ?
<a href="#" className="danger flat-button" onClick={this.handleBlock}>
<i className="material-icons">block</i> &nbsp;{formatMessage(messages.block_contact)}
</a>
:
null
}
{!this.state.owner ?
<a href="#" className="danger flat-button" onClick={this.handleReport}>
<i className="material-icons">report</i> &nbsp;{formatMessage(messages.report_chat)}
</a>
:
null
}
</div>
<div className="hr" />
<div className="panel-form-column">
{this.state.groupTopic ?
<div className="group">
<label>
<FormattedMessage id="label_your_permissions" defaultMessage="Your permissions:"
description="Label for current user permissions" />
</label>
<tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'want')}>
{this.state.access}
</tt>
</div>
:
<div className="group">
<div>
<label className="small">
<FormattedMessage id="label_permissions" defaultMessage="Permissions:"
description="Section title" />
</label>
</div>
<div className="quoted">
<div>
<FormattedMessage id="label_you" defaultMessage="You:"
description="Label for the current user" /> &nbsp;<tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'want')}>
{this.state.access}
</tt></div>
<div>{this.state.fullName ? this.state.fullName : formatMessage(messages.other_user)}:
&nbsp;<tt className="clickable" onClick={this.handleLaunchPermissionsEditor.bind(this, 'given')}>
{this.state.modeGiven2}
</tt>
</div>
</div>
</div>
}
<div className="group">
<div>
<label className="small">
<FormattedMessage id="label_default_access_mode" defaultMessage="Default access mode:"
description="Label for default access mode" />
</label>
</div>
<div className="quoted">
<div>Auth: <tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'auth')}>{this.state.auth}</tt></div>
<div>Anon: <tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'anon')}>{this.state.anon}</tt></div>
</div>
</div>
</div>
</div>
);
}
};
export default injectIntl(TopicSecurityView);